Skip to main content

Caller identification

Overview

Motoko’s shared functions support a simple form of caller identification that allows you to inspect the ICP principal associated with the caller of a function. Principals are a value that identifies a unique user or canister.

You can use the principal associated with the caller of a function to implement a basic form of access control in your program.

Using caller identification

In Motoko, the shared keyword is used to declare a shared function. The shared function can also declare an optional parameter of type {caller : Principal}.

To illustrate how to access the caller of a shared function, consider the following:

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.

The calls to the inc() function do not change. At each call site, the caller’s principal is provided by the system, not the user. The principal cannot be forged or spoofed by a malicious user.

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

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

Adding access control

To extend this example, assume you want to restrict the Counter actor so it can only be modified by the installer of the Counter. To do this, you can record the principal that installed the actor by binding it to an owner variable. You can then check that the caller of each method is equal to owner like this:

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

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 while 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}) 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 is both sharable and stable, meaning you can compare Principals for equality directly.

Below is an example of how you can record and compare principals.

import Principal "mo:base/Principal";
import HashMap "mo:base/HashMap";
import Hash "mo:base/Hash";
import Error "mo:base/Error";

actor {
// Initialize a stable variable to store principals
private stable var principalEntries : [(Principal, Bool)] = [];

// Create HashMap to store principals
private var principals = HashMap.HashMap<Principal, Bool>(
10,
Principal.equal,
Principal.hash
);

// Check if principal is recorded
public shared query(msg) func isRecorded() : async Bool {
let caller = msg.caller;
switch (principals.get(caller)) {
case (?exists) { exists };
case null { false };
};
};

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

principals.put(caller, true);
true;
};
};
Logo