Note: This is a demonstration project and hasn't been audited. Please use it for inspiration only.
The Internet Computer has a built-in mechanism for access control based on cryptographic signatures. The canister can be sure that its method was called by a specific principal, which is available as ic_cdk::api::caller()
. However, the canister needs also to know which permissions this principal has.
In microservice architectures, it's common to centralize access control by having a single authorization service that manages all permissions. This simplifies permission management but raises the question of how resource services learn about permissions. There are two main patterns:
- Tokens: A client requests an authorization token from the authorization service and invokes it at the resource service. Here, the resource service does not need to directly communicate with the authorization service.
- Validation endpoint (Inter-canister call): The authorization server exposes a validation endpoint that the resource service can use to validate permissions.
On the Internet Computer, we can use the same patterns. This demonstration project showcases both patterns and provides a comparison.
We have the following two canisters:
The authorization canister has the following interface:
type token = blob;
type target = text;
service : {
"update_permissions": (principal, target, bool) -> (text); // update permissions of a specific user and target (by its function name)
"read_permissions_certified": () -> (opt token) query; // fetch permissions as token
"verify_permissions": (principal, target) -> (bool, nat) query; // verify permissions
}
The permissions are maintained in a certified data structure using the ic-certified-map
crate and the certified data functionality of the Internet Computer. When a client fetches the token with the read_permissions_certified
function, then this token includes a path to the state root hash of the Internet Computer signed by the subnet together with a delegation from the NNS subnet. Thereby, the resource canister can verify the authenticity of the token given the IC's public key and the client can't tamper with the token.
The token also includes a timestamp to validate freshness.
The resource canister is the counter example canister with added permissions.
type token = blob;
service : (principal) -> {
"get": (opt token) -> (nat, nat) query;
"get_composite": (opt blob) -> (nat, nat) query;
"set": (nat, opt token) -> (nat);
"inc": (opt token) -> (nat);
The second number in the return values of the first two methods and the number in the return values of the last two methods is the number of instructions used for the verification of the permissions.
Note that we have to provide a principal as an init argument. This allows registering the authorization canister. We need the principal of the authorization canister to verify that the tokens have been "signed" by the authorization canister, or to know how to call the verify_permissions
endpoints.
Furthermore, we note that each endpoint has an optional argument to provide the authorization token and that there's an additional endpoint called get_composite
. This is a composite query that allows performing an inter-canister query call to the verify_permissions
endpoint.
Token | Inter-canister Call | |
---|---|---|
DevX | 🤔 | 😀 |
Latency | 😀 | 😀 (same subnet) |
Cost | 🙂 | |
Security | 😀 | 😀 |
The token-based approach adds quite a bit of complexity. As a client, we need to fetch the authorization token and have logic to get a new one if it is expired. Furthermore, there we need to verify the permissions in the certified map, as well as verify the IC certificate. This could be abstracted away in a library though. The token-based approach is also currently out of reach for Motoko developers, since we don't have a library for a certified map, as well as a library for BLS signature verification.
The inter-canister call to the validation endpoint, on the other hand, is very easy to implement in Rust and Motoko.
As long as both canisters are on the same subnet there is no big difference in latency between the two approaches. With the availability of composite queries, it could be even faster to have a composite query than to do the token verification. If the canisters were on different subnets, then the token-based approach would provide lower latency.
On the cost side, let's look at the token approach first:
- Token size: 221 bytes
- Ingress msg byte cost: 1'000 * 221 = 221'000 cycles (we don't need to take the cost for the ingress message itself into account, since it is needed for both approaches)
- Instructions for certificate verification: 463'975'738 ~> 185'590'000 cycles
Total: 185'811'000 cycles
For the inter-canister call approach, we have a call and a reply, which cost 260'000 cycles each + 3 times the cost for update message execution of 590'000 cycles => 2'290'000 cycles. The data transferred is rather small, so we neglect this cost hee.
The instructions used for verification of the permissions by the authorization canister are also quite small with about 50'000 instructions => 20000 cycles
Total: 2'310'000 cycles
=> The token-based approach is approx. 80 times as expensive as the inter-canister call-based approach.
Note: You can find the detailed list of all costs on the IC here. I've assumed a normal application subnet with 13 nodes.
There are two points to mention concerning security:
- The inter-canister call approach has the advantage that permissions can immediately be revoked (if there's no caching).
- You need to be aware of the implications of inter-canister calls in general. See the relevant section in the Security Best Practices.
You need to have the command line tool dfx
installed:
sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)"
You can run a demo flow that will deploy the canisters, set some permissions, fetch a token and perform some operations on the resource canister with and without the authorization token. Furthermore, the runtime of the operations will be written to the terminal.
You can run the demo flow with the following command:
./demo.sh
I've deployed the canisters to the IC on a single subnet. You can interact with the deployed canisters:
Resource canister: 62hqk-naaaa-aaaap-qa5oa-cai
Authorization canister: 65gw6-ayaaa-aaaap-qa5oq-cai
or deploy your own instances.
Some example calls (or a complete flow) can be found in demo_ic.sh
.
The authorization canister does not use stable memory, as such all permissions are lost on upgrade.
If you want to work on production-ready libraries or a configurable authorization canister, then get in contact with me or apply for a DFINITY Developer Grant.
Check out https://internetcomputer.org.