How to act on canister's behalf?

My real situation simplified:

I have an app A that allows everybody to create a frontend (assets) canister F and backend canister B.

I’m having a hard time to code user’s ability to create a bookmark storing F # ":" # B in a bookmark.

The problem is that the bookmark should be accessible by the user’s a principal in app A. To reliably authenticate that the user has this principal, need to do it from app A. But to initialize the canister B with a correct owner’s (user’s) principal, need to initialize B in F (not in A) because the owner’s principal is seen only from F. So, only F can initialize B, but this is after the user already left A. How to get value B in A without allowing a hacker to break it?

The best idea that I came up with is: In app A create the canister B but don’t install_code. So, in F we have the principal of B (to compute F # ":" # B). And in F we initialize B with a correct owner. But this idea is convoluted.

Is there a straightforward way to solve this? (maybe, something with identity delegation?)

You can’t just block unauthorized callers by checking the canister_id of A to call B and just pass in the user principal as an argument?

Canister A
#[update]
pub async fn foo(bookmark: String) -> Result<(), String> {
  canister_b::store_bookmark(caller(), bookmark).await
}

Canister B
#[update]
pub fn store_bookmark(owner: Principal, bookmark: String) -> Result<(), String> {
   if caller() == canister_b_id {
     Bookmarks::save(owner, bookmark)
  }
}

Canister B in our code could be able to save bookmark to another user, if B would be replaced by another code. That’s not good.

Does A have a front end and a backend?

When you say “users principal in …” then do you mean principal obtained from II login?

Are the frontends A and F related to each other, so would it be ok if the user gets the same principal from II in both?

Where do you want to store the bookmarks (in which canister)?

Yes, A has a front end and a backend.

“do you mean principal obtained from II login?” Yes.

“Are the frontends A and F related to each other, so would it be ok if the user gets the same principal from II in both?” No, because F and B can be created in unlimited quantities by my system and likewise can be created by a hacker who substitutes to B or F malicious code.

Suppose, bookmarks are stored in A.

So is this flow correct (though not optimal in terms of number of step the user has to do):

  • user logs in on A frontend and initiates the flow
  • A backend creates canisters B and F, B is still locked (doesn’t allow access from any user)
  • user logs in on F and copies principal to clipboard
  • user goes back to A frontend and pastes the principal
  • A backend stored bookmark (F,B) for the pasted principal
  • A backend configured B to give access to the pasted principal

Not sure, is that in principle what you want to do?

In your flow, the user can paste a principal of another user, so confusing him with incorrect bookmark data appearing of the sudden.

Also copy&paste is a bad solution.

Agree. Just wanted to understand a hypothetical solution first that is correct. Then I will propose an improvement that doesn’t require copy & paste.

You mean accidentally or maliciously? Accidentally will be removed by a solution without copy&paste. Maliciously I would say isn’t possible because the user is still logged in on A in the same session that created F an B. So the pasted principal is securely linked to the user on A, to F and to B. You cannot take over someone else’s F and B.

It can be done maliciously, because a user can create a fake (not created by A) pair F:B. Then he uses a wrong idenitity. Also, pasted principal cannot be securely linked to the user on A, because the user can paste any principal.

What do you mean by “the same sessions”? Principals in A and B are different for the same user.

But A (the backend) knows which F:B it created. It won’t accept or deal with F:B that it didn’t create.

I mean:

  • the user logs into A’s frontend with principal P1, i.e. the user now makes calls to A’s backend as P1
  • user initiates the creation flow
  • A’s backend creates F and B and stores the map P1 → (F,B). This means that F,B were created for user P1 and A’s backend knows that
  • User opens a separate tab and logs into F , gets principal P2
  • User goes back to the previous tab and pastes P2
  • A configures B to give access to P2 only
  • A can also store the map P1 → (F,B,P2) (not sure if A would need that information again in the future or if its job is done after the previous step)

If the user pastes the wrong principal he won’t have access to B.
If I create my own F:B then A will never interact with it.

Is this flow correct (though not optimal) or not?

It seems not correct: A user can paste another user’s principal instead of P2.

You have “A configures B to give access to P2 only” with an arbitrary P2. This seems wrong. Or do I misunderstand you?

The user is logged in as P1 when he pastes P2. So the call from A’s frontend to A’s backend to submit P2 is made as identity P1. It is not an anonymous call. A’s backend has the map P1 → (F,B) stored. It looks up who called (which is P1), then finds (F,B), then configures B to give access to the pasted principal.