Skip to main content

Security best practices: Identity and access management

Intermediate
Security
Concept

Make sure specific user actions require authentication

Security concern

If this is not the case, an attacker may be able to perform sensitive actions on behalf of a user, compromising their account.

Recommendation

  • The caller of every canister call the caller can be identified. The calling principal can be accessed using the system API’s methods ic0.msg_caller_size and ic0.msg_caller_copy (see here). If an identity provider such as Internet Identity is used, the principal is the user identity for this specific origin. If some actions (e.g. access to user’s account data or account specific operations) should be restricted to a principal or a set of principals, then this must be explicitly checked in the canister call. An example in Rust can be found below:
// Let pk be the public key of a principal that is allowed to perform
// this operation. This pk could be stored in the canister's state.
if caller() != Principal::self_authenticating(pk) { ic_cdk::trap(...) }

// Alternatively, if the canister keeps data for different principals
// in e.g. a map such as BTreeMap<Principal, UserData>, then the canister
// must ensure that each caller can only access and perform operations
// on their own data:
if let Some(user_data) = user_data_store.get_mut(&caller()) {
// perform operations on the user's data
}
  • In Rust, the ic_cdk crate can be used to authenticate the caller using ic_cdk::api::caller. Make sure the returned principal is of type Principal::self_authenticating and identify the user’s account using the public key of that principal. See the example code above.

  • Do authentication as early as possible in the call to avoid unauthenticated actions and potentially expensive operations before authentication. It is also a good idea to deny service to anonymous users.

  • Do not rely on authentication performed during ingress message inspection.

Disallow the anonymous principal in authenticated calls

Security concern

The caller from the system API (e.g. ic0::api::caller in Rust) may also return Principal::anonymous(). In authenticated calls, this is probably undesired, and could have security implications, since this would behave like a shared account for anyone that does unauthenticated calls.

Recommendation

In authenticated calls, make sure the caller is not anonymous and return an error or trap if it is. This could be done centrally by using a helper method. An example in Rust can be found below:

fn caller() -> Result<Principal, String> {
let caller = ic0::api::caller();
// The anonymous principal is not allowed to interact with canister.
if caller == Principal::anonymous() {
Err(String::from(
"Anonymous principal not allowed to make calls.",
))
} else {
Ok(caller)
}
}

Do not rely on ingress message inspection

Security concern

The correct execution of canister_inspect_message is not guaranteed because it is executed by a single node and if that node is malicious, it can simply skip this check. In that case the update call would be executed without any message inspection checks.

Also note that for inter-canister calls canister_inspect_message is not invoked.

Recommendation

Your canisters should not rely on the correct execution of canister_inspect_message. This in particular means that no security critical code, such as access control checks, should be solely performed in that method. Such checks must be performed as part of an update method to guarantee reliable execution. Ideally, they are executed both in the canister_inspect_message function and a guard function.

Use a well-audited authentication service and client side ICP libraries

Security concern

Implementing user authentication and canister calls yourself in your web app is error prone and risky. For example, if canister calls are implemented from scratch, there may be bugs around signature creation or verification.

Recommendation

  • Consider using and identity provider such as Internet Identity for authentication, use agent-js for making canister calls, and the auth-client for interacting with Internet Identity from your dapp.

  • You may consider alternative authentication frameworks on ICP for authentication.

Set an appropriate session timeout

Security concern

Currently, Internet Identity issues delegations with an expiry time. This expiry time can be set in the auth-client. After a delegation expires, the user has to re-authenticate. Setting a good value is a trade-off between security and usability.

Recommendation

See the OWASP recommendations. A timeout of 30 minutes should be set for security sensitive applications.

The auth-client supports idle timeouts.

Don’t use fetchRootKey in agent-js in production

Security concern

agent.fetchRootKey() can be used in agent-js to fetch the root subnet threshold public key from a status call in test environments. This key is used to verify threshold signatures on certified data received through canister update calls. Using this method in a production web app gives an attacker the option to supply their own public key, invalidating all authenticity guarantees of update responses.

Recommendation

Never use agent.fetchRootKey() in production builds, only in test builds. Not calling this method will result in the hard coded root subnet public key of the mainnet being used for signature verification, which is the desired behavior in production.