I have decided to use Internet Identity as the authentication method for my dapp. On the frontend I am using the provided DFINITY packages to manage whether a user is logged in or not. However, I am unsure how to manage authenticated users on my backend (Motoko canisters). I want my canisters to be able to authenticate that a user request is coming from someone who is actually signed into my dapp. Essentially, preventing outsiders from hitting off my public canister routes.
When my dapp issues a user a unique Principal ID, does that user only have access to that Principal ID while authenticated with my dapp? Is it possible they could manipulate a canister request such that the Principal Id of msg.caller is their Principal ID within my dapp?
Here is my current thought process - If a user can only access the Principal ID issued to them from my dapp when signed in, then I can just maintain a list of user accounts and check that the msg.caller is in that list. But, I am still not sure this is the best approach. Is there any kind of session token I can generate on the client side by the signed in user?
Am I on the right track here? Does anyone have any advice?
You can create a user profile for each new user, and associate it with that user’s principal. That way you can manage the user’s in the backend.
From the frontend, you need to create a protected route to prevent unregistered users from accessing certain endpoints.
So say I had a canister called “User Profile” that was devoted to tracking user Principal IDs that have authenticated with my dapp. Then on all my other canisters I would have to make a call to the “User Profile” canister in order to check if the Principal ID passed in msg.caller has been registered on my backend. Is this the most efficient way to verify users? Is there any sort of session token I can authenticate without having to make a call to a different canister?
Then there is also a concern for the actual “User Profile” canister. If I have a public endpoint responsible for receiving new user data (Principal ID/User data combo), then how do I protect that route? I cant check if the user exists on my “User Profile” canister when they go to register themself for the first time. And if I have no protection on that route then anyone with my “User Profile” canister ID can spam new user information into it. How do I handle the initial user creation process?
That isn’t possible. You public canister methods are always open to anyone, at least until the point of your internal access control which is inspecting msg.caller. Your approach of maintaining an allow list for msg.caller also won’t work because how do you create that list? How do you authenticate a user the first time around to put him on the list? If a user of your dapp frontend can get on the list then anyone else also can.
Just curious because I have never heard of this request. Why do you want to restrict your canister methods to users of your frontend?
At the tip of my head, I would say common patterns for this, regardless of the technology which includes a frontend application served in a browser, are captcha and invitation codes.
Yes, I totally agree with the concerns you have presented.
As for why I would like to protect my canister routes - My dapp is going to need to store a lot of user uploaded pictures. So I am trying to set up an infinitely scalable decentralized storage system. My idea was to use a management canister that was able to create and track the DFINITY pre configured Asset Canister. Every Asset Canister would also be assigned a logic canister, which has functionality to track which assets belong to which users and essentially authenticate requests to delete or modify existing assets. It also has functionality to temporarily give users the “Prepare” permission so that they can propose an asset for upload.
I would like it so that only users from my frontend are allowed to interact with these routes to prevent third parties from abusing my asset canisters for their own personal storage, and to even prevent real users of my dapp from using their principal ID to upload their own personal assets. And therefore I would like verify that every asset request originated from my frontend.
However, I still dont know that I am even on the right track here. Do you know of a better way I can create an infinitely scalable decentralized storage solution on my dapp that meets the requirements I set above? Any help would be much appreciated, thank you.
What is the difference here? Do you mean you want to prevent users from uploading things that are not images? Then maybe you can make the canister inspect the file. For example you can put a size limit on the file. Or you inspect the tag bytes at the beginning of the file to check if matches the file types that you want to allow.
Usually, the idea of canisters is to be open and composable. Open so that anyone can write a frontend for them and the user is not bound to a specific frontend. Or that the canister can be called from the command line. And composable so that other canisters can be “users”, i.e. the public methods can be called from other canisters. Usually, you would track the amount of data that each user has uploaded and then find a way to charge the user for it. That will prevent abuse.
A side question: this asset canister was specifically made to serve assets over http. Is that something you want? Do you want the images to be accessible via http directly without your frontend?
Yes, this is exactly what I want. The images uploaded are meant to be viewed by everyone on the dapp. Therefore, I wanted to use the asset canister so that I can easily reference uploaded images on my frontend with by their url hosted by the DFINITY Asset Canister.
Along the same lines. Since the images are for the greater good of the dapp (not for individual user’s personal use) I would prefer not to charge people for uploads. Just like how Facebook does not charge you to upload a profile picture and banner to your profile. I am just trying to think if there is any way to ensure that these open and “free” (to the user) canisters do not get abused. I figured if there was some way to ensure a user was making the request from my frontend then I could be confident the canisters are being used properly.
For storage it is $5 per GB per year right? So to scale up to 100 GB would only cost $500 per year right? If so, I do not feel the need to charge the user as this is a reasonable cost.
Do you have a second canister available to track users? I mean the standard asset canister plus a second canister plus the frontend? It is only of practical relevance so that you don’t have to modify the asset canister much. It does not change fundamentally the problem that you have.
Fundamentally, you have the same problem with spam that Facebook has. Anybody can reverse engineer Facebook’s web app or native mobile app and call the endpoints directly that Facebook’s backend provides. Facebook’s backend has some data points to detect and block spam such as:
the user’s IP address
the user’s login
the history and behavioral pattern of the login
age of the login
social connections of the login
the file size being uploaded
the file itself to inspect
captchas if presented
If your backend is a canister then it has (or can have if you make it so) the same information with one excepetion. You don’t know the IP address from which the request originates because the IC does not tell the canister from which IP address it is being called.
Another difference is that the new Principals are easy to create but new Facebook logins are harder to create. So you do have some disadvantages compared to Facebook but you still have a lot of options.
For example the Internet Identity uses canister-created captchas to limit spam. You can start with something simple and then over time improve the code and make your spam protection more and more sophisticated. For example you can rate limit your backend and only allow N megabytes per minute to be uploaded (from any Principal). That way an attacker has to be active over a long time in order to spam your backend. By the time you have more users you can come up with something better. You can also integrate with DecideAI to check if the Principal belongs to a unique human. Then you can give such verified Principals a larger quota then a non-verified one.