Skip to main content

Caller identification

On ICP, every user and canister has a unique principal identifier. When a canister receives a message, such as a function call, the principal identifying the caller of the function is included in the message.

The caller’s principal is provided by the network, not the user or canister. A principal cannot be forged or spoofed by a malicious user.

Motoko’s shared keyword is used to declare a shared function. Shared functions support a simple form of caller identification that can be used to inspect incoming messages and implement access control based on the caller's principal. The shared function can also declare an optional parameter of type {caller : Principal}.

For example, when a shared function is called by a user or another canister, the caller's principal can be captured with msg.caller:

shared(msg) func inc() : async () {
// ... msg.caller ...
}

In this example, the shared function inc() specifies a msg parameter, a record, and the msg.caller accesses the principal field of msg.

To access the caller of an actor class constructor, you use the same syntax on the actor class declaration:

shared(msg) persistent actor class Counter(init : Nat) {
// ... msg.caller ...
}

Adding access control

Access control can be added to an application by recording a caller's principal and then introducing control rules, such as a list of allowed principals that are permitted to call the function. If the caller's principal is not within the list, their call to the function will be rejected.

For example, if you have an actor called Counter, you can record the principal that installed the actor by binding it to an owner variable. Then, you can implement a check that verifies that the caller of each method the actor exposes is equal to the principal stored in owner:

shared(msg) persistent actor class Counter(init : Nat) {

transient let owner = msg.caller;

var count = init;

public shared(msg) func inc() : async () {
assert (owner == msg.caller);
count += 1;
};

public func read() : async Nat {
count
};

public shared(msg) func bump() : async Nat {
assert (owner == msg.caller);
count := 1;
count;
};
}

In this example, the assert (owner == msg.caller) expression causes the functions inc() and bump() to trap if the call is unauthorized, preventing any modification of the count variable. However, the read() function permits any caller.

The argument to shared is just a pattern. You can rewrite the above to use pattern matching:

shared({caller = owner}) persistent actor class Counter(init : Nat) {

var count : Nat = init;

public shared({caller}) func inc() : async () {
assert (owner == caller);
count += 1;
};

// ...
}

Simple actor declarations do not let you access their installer. If you need access to the installer of an actor, rewrite the actor declaration as a zero-argument actor class instead.

Recording principals

Principals support equality, ordering, and hashing, so you can efficiently store principals in containers for functions such as maintaining an allow or deny list. More operations on principals are available in the principal base library.

The data type of Principal in Motoko supports equality, ordering, and hashing. Principals both sharable and stable, meaning you can compare them for equality directly.

Below is an example of how you can record a function's caller Principals in a set and check if a caller is already in the set:

import Principal "mo:base/Principal";
import OrderedSet "mo:base/OrderedSet";
import Error "mo:base/Error";

persistent actor {

transient let principalSet = OrderedSet.Make<Principal>(Principal.compare);

// Create set to record principals
var principals : OrderedSet.Set<Principal> = principalSet.empty();

// Check if principal is recorded
public shared query(msg) func isRecorded() : async Bool {
let caller = msg.caller;
principalSet.contains(principals, caller);
};

// Record a new principal
public shared(msg) func recordPrincipal() : async () {
let caller = msg.caller;
if (Principal.isAnonymous(caller)) {
throw Error.reject("Anonymous principal not allowed");
};

principals := principalSet.put(principals, caller)
};
};
Logo