Hi everyone, we are building an icrc7 (motoko) NFT collection that we want to integrate with our Next JS front end. We are in need of implementing access control for multiple functions that can be called only by our web app (not by individual users)
if(msg.caller != state.admin) return #err("unAuthorized");
I’m looking for the safest way to provide an identity to the web app. I searched through the forum but couldn’t find an exact answer.
I could find a resource from Kai: [Access Controls Tutorial], but that is aimed at user access control and not specifically for the web app.
I have implemented a working prototype by using dfx to set the deployer of the contract as the admin and then using the decrypted Pem file to import the identity to the web app.
const rawKey = fs.readFileSync("./key.pem").toString();
const buf = pemfile.decode(rawKey);
const identity = Secp256k1KeyIdentity.fromSecretKey(buf.subarray(7, 39));
I have seen from other posts that providing identity to the web app is a security concern. In that case, can anyone please guide me on the secure way to provide access control to the front end app.
There can be multiple ways to do this, one possible way you already discussed above, but that is surely not the safest way due to security concerns.
One other way can be to introduce http GET/POST requests from your canister to all the endpoints you want to secure and then use auth headers to securely call those canister endpoints from the web app.
1 Like
Hi, thank you for replying and suggesting canister outcalls as an alternative. Considering all this, I have came up with kind of a proxy model to make this more secure. Can I make any modification to make this more secure?
ICRC7 Contract
The ICRC7 contract is an Actor class which would be deployed by another contract (let’s call it the admin contract) and hence the admin contract would be the owner of ICRC7 and only it can call access admin functions (Write functions) in ICRC7
Admin Contract
-
There are two access control levels in the admin contract
a) Owner: Single identity with the highest level of access
b) Authorized principals: List of identities with access to the write functions of ICRC7 (limited access)
-
The deployer of the admin contract (controller identity) is set as the owner of the admin contract (Highest level access control)
-
Only the owner of admin contract can add authorized or remove authorized principals.
-
This way a new Identity without any cycles can be added as an authorized principal and the identity can be exported to the Next JS server
Next JS
- When the user performs a write action in the client, a POST request is made to the server
- The server has access to the private key of the authorized identity and would make the call through a custom JS agent of that identity
const rawKey = process.env.RAW_KEY.toString();
const buf = pemfile.decode(rawKey);
const identity = Secp256k1KeyIdentity.fromSecretKey(buf.subarray(7, 39));
- The private key is never made available to the client and should be more secure
- Fail safe - Even in the case if the private key from the server gets compromised, it is a dummy identity without any tokens/cycles which only has write access to the ICRC7 contract, the owner identity can remove the compromised identity from the authorized identities in the admin contract
1 Like
Nice approach. Only thing is maintaining a proxy server in between for key management else everything looks good to me.
Although you can eliminate icrc-7 admin contract and write all this logic inside main contract itself. Where you can deploy contract with 1 owner and multiple admins. Owner key will be saved in server and will be able to edit/update admins and than access control owner and admin endpoints using caller of the endpoints.
1 Like
Thank you for suggesting a single canister approach. I would love to use a single contract but I think it would not be possible due to how the icrc7 standard is built. For example, taking a look at the mint function
public func set_nfts<system>(caller : Principal, request : SetNFTRequest, requireOwner: Bool) : Result.Result<[SetNFTResult], Text> {
if (caller != state.owner) { return #err("unauthorized") };
It only allows for a single state owner. This means that if I don’t use a proxy canister, I would have to use that single identity in the server to create an NFT and if that identity is compromised, the contract is unrecoverable.
I’m not fond of using the proxy canister approach as it is leading to extra cost. But due to the icrc7 architecture, I feel like I don’t have a choice but to use a proxy model where the proxy is the single owner of the icrc7 and the owner of the proxy is a secure off the grid identity which manages a list of authorized identities which then would be stored and used in the server
But, there is a very unprofessional and hacky way to change this
Instead of calling the set nft function with the caller
const result = icrc7().set_nfts<system>(msg.caller, [req], true)
I could first do the authorization and then call the set_nfts
function with a fixed principal
if (!authorized(msg.caller) return #err("Unauthorized")
const result = icrc7().set_nfts<system>(icrc7().state.owner, [req], true)
This solves the multiple authorization trouble. But I don’t know about the security implication of this approach
1 Like