Account management using the Root Pico

Compatibility

The page applies only to earlier versions of the pico-engine, and does not apply to version 1.0 of the pico engine.


After you have installed your node pico engine (see Pico Engine Quickstart for instructions), you begin to operate it. Anyone who can access the URL of your engine has full access to all of the picos hosted on your engine.

You may wish to require a password-protected login. This functionality is built-in to the engine, starting at version 0.12.9, and is described herein.

Principles of operation

With account management, each immediate child pico of the Root Pico is treated as an Owner Pico. The name of each Owner Pico becomes the "owner identifier" (known in the KRL code as owner_id) and is associated with an authentication method. A directory of owner picos is maintained by a ruleset which is installed in the Root Pico. An associated ruleset must be installed in each owner pico.

The developer UI identifies the currently logged-in owner pico by having its pico identifier saved in the JavaScript-provided sessionStorage using the key owner_pico_id.

With an account management ruleset installed in the Root Pico, the UI will require login to access either the Root Pico or any of the registered owner picos. The UI provides a two-step interface for login, with an initial form requesting entry of the owner id, and a second form requesting entry of the associated password.

Login form

Password form

Code words form

The UI provides a second method of authentication, via "code words". If an owner elects to use this method, then when she has entered her owner id, she will be presented with this form, instead of the password collection form. She will use a mechanism described later to obtain the code words that are current at that time, and enter them into this form.

Creating an owner pico

A person who wishes to become an owner on your pico engine will click on the link "Need an owner pico?" whereupon he will be presented with this sign-up form.

He will enter the desired name of his owner pico which will also serve as his owner id, and then select an authentication method and, if applicable, an initial password.

Since John has chosen "code words" as his authentication method, he will be presented with this important information, which he must save for future logins. He can right-click on the QR code image and select "Save Image As..." and put the image in a safe place, perhaps even printing it. Or he may bookmark the link in his browser.

Once having saved the URL which will provide code words, he will click the "Got it" link to complete the creation of his owner pico. Later on, when he logs in, he will visit this URL (perhaps by scanning the QR code) to receive the current code words.

Changing a password

An owner can change her password in the "About" tab of her owner pico. Here we show this form in the context of the Root Pico. As the owner and operator of your node pico engine, you should manually change the Root Pico password, using this form, in order to secure your system.

Rulesets for managing owner picos

You must provide both the account management ruleset for the Root Pico, and an associated account ruleset for the owner picos.

We'll show you how these rulesets can be written, and we provide reference rulesets. These two rulesets are referred to in this page as the "reference account management ruleset" and the "reference account ruleset". You may use these rulesets as-is, or you may create your own. To use them as-is, you would go to the "Rulesets" tab of your Root Pico in the UI, enter this URL into the box and click the button "install ruleset from URL".

When a ruleset is installed in the Root Pico, the event pico:ruleset_added is sent to the Root Pico. This event may be handled by your account management ruleset (if not, you will need to initialize things in some other way). The reference ruleset reacts to this event in the way shown here.

  rule owner_initialize {
    select when pico ruleset_added
    if not ent:owners then noop();
    fired {
      ent:owners := {};
    }
  }

  rule pico_ruleset_added {
    select when pico ruleset_added rid re#temp_acct_mgr#
    every {
      engine:installRuleset(url="temp_acct.krl", base=meta:rulesetURI);
      engine:newChannel(name=time:now(),type="to owner") setting(new_channel);
    }
    fired {
      raise owner event "admin" attributes { "txnId": meta:txnId };
      ent:owners{"Root"} :=
        { "pico_id": meta:picoId,
          "eci": new_channel{"id"},
          "dname": "Root Pico",
          "method": "password"
        };
    }
  }

The reference account management ruleset takes advantage of this event to initialize its entity variable, and your ruleset may do the same. The ruleset must install an account ruleset into the same pico (the Root Pico functions as an Owner Pico), and may send it an event, such as the owner:admin event used in the reference rulesets, to initialize it. If this is done, it should secure the event by including a one-time value, such as the transaction ID as done by the reference rulesets. Finally, the account management ruleset must record the information it will need later to respond to the owner:eci_requested event. Your rulesets should use a new ECI for each owner pico, for the purpose of communicating from the Root Pico to owner picos.

The reference account ruleset reacts to the owner:admin event as follows. The reference rulesets establish the owner ID "Root" and initial password "toor" but your rulesets may make different choices. You should manually change the Root Pico password to secure your system.

  rule owner_admin {
    select when owner admin
    pre {
      txnId = event:attr("txnId");
      legit = (txnId == meta:txnId);
    }
    if legit then noop();
    fired {
      ent:owner_id := "Root";
      raise owner event "pwd_needs_encoding" attributes { "password": "toor" };
    }
  }

Account Management ruleset

The account management ruleset must react to these events, as described below:

  • owner:eci_requested 
  • owner:creation
  • information:child_deleted

Event owner:eci_requested

This event serves two purposes. First, the UI sends it, with no attributes, to determine whether there is an account management ruleset (if not, then the login feature is disabled, and everyone has access to all picos). The rule in this case must return a directive with at least a name. The directive itself is used by the UI to set options, and its presence signals that you do require owner login, and promises that your rulesets meet the other requirements described in this document. The available options are: immediateLogin which you may set to true if you prefer that the owner of a newly created owner pico be logged in after creation (otherwise the new owner will be required to log in at that point), and rid which you may set to the ruleset identifier of your account managment ruleset. The rid option is not currently used, but may be used by the UI in the future.

This is the rule from the reference ruleset. Although we mention it first here, it must be placed last in the account management ruleset.

  rule owner_pico_options {
    select when owner eci_requested
    send_directive("options",{"immediateLogin":true,"rid":meta:rid});
  }

The second, and primary purpose of this event is that the UI causes the engine to send it to the Root Pico whenever someone presses the "Login" button from the Login form. This event will have a single attribute, owner_id, which is what the person entered into the Login form. Your ruleset must return a directive, whose name is ignored but should be the same whether the owner_id is known or not. If the owner_id is known, the directive options must include pico_id, ecinonce, and method. The directive options may include owner_id. If successful, your ruleset must send the event owner:eci_provided to the owner pico, which must include as attributes a random nonce, and may include other items.

These two rules from the reference account management ruleset illustrate these requirements. Notice that these rules need to appear before the option-providing rule, and that they include the last statement in their fired clauses, so that only one of these three rules will fire when the owner:eci_requested event is recieved. These rules use an entity variable, ent:owners, to keep track of the owner picos. Your ruleset may also use this entity variable, but must somehow keep track of the owner picos, and may record other information, such as the last login attempt information, as stored here in the entity variable, ent:lastOne.

  rule find_owner_pico_by_name {
    select when owner eci_requested
    pre {
      owner_id = event:attr("owner_id");
      entry = ent:owners{owner_id};
      pico_id = entry{"pico_id"};
      eci = entry{"eci"};
      method = entry{"method"};
      nonce = random:word();
      options = {"owner_id":owner_id,"pico_id":pico_id,"eci":eci,"method":method,"nonce":nonce};
    }
    if eci then every {
      send_directive("here it is",options);
      event:send({"eci":eci, "domain":"owner", "type":"eci_provided", "attrs":options});
    }
    fired {
      last;
      ent:lastOne := options;
    }
  }

  rule owner_pico_not_found {
    select when owner eci_requested
    if event:attr("owner_id") then
    send_directive("here it is",{"owner_id":event:attr("owner_id"),"method":"password"});
    fired {
      last;
    }
  }

Event  owner:creation

This event is sent to the Root Pico by the engine, at the request of the UI, when a new owner clicks the "Submit" button on the account creation form. It includes the attributes owner_id (labelled in the form "Pico name") and method. If the method is "password", then it also includes an attribute for the owner-supplied password.

Your account management ruleset must record this information in an entity variable. It must check for uniqueness of the owner_id. Your ruleset must create a new owner pico as a direct child of the Root Pico. Upon successful creation, your ruleset must send a directive whose name may be "new owner pico" (but must not be "new owner pico code query channel") with options which must include pico_id and eci, and may include other values. Finally, if the owner selected the authentication method "code words", your ruleset must send a directive whose name must be "new owner pico code query channel" with options which must include eci and may include other values. The eci provided should be one specially created for the purpose of providing the current code words to the owner, as described below in the section entitled "Function code".

The account management reference ruleset fulfills these requirements as follows.

  rule owner_creation_owner_id_uniqueness_guard {
    select when owner creation
    pre {
      owner_id = event:attr("owner_id");
    }
    if ent:owners >< owner_id then
      send_directive("owner_id already in use",{"owner_id": owner_id});
    fired {
      last;
    }
  }

  rule owner_creation {
    select when owner creation
    fired {
      raise pico event "new_child_request" attributes event:attrs();
    }
  }

  rule pico_new_child_created {
    select when pico new_child_created
    pre {
      child_id = event:attr("id");
      child_eci = event:attr("eci");
      owner_id = event:attr("owner_id");
      method = event:attr("method");
      code = method == "code";
      dname = event:attr("dname") || owner_id;
    }
    every {
      engine:installRuleset(child_id, url="temp_acct.krl", base=meta:rulesetURI);
      event:send({"eci":child_eci, "domain":"owner", "type":"creation", "attrs":event:attrs()});
      engine:newChannel(child_id,time:now(),"to owner") setting(new_channel);
      send_directive(
        "new owner pico",
        { "owner_id": owner_id,
          "pico_id": child_id,
          "eci": new_channel{"id"},
          "method": method});
    }
    always {
      raise owner event "new_owner_pico_with_code"
        attributes event:attrs().put({"owner_pico_id":child_id}) if code;
      ent:owners{owner_id} := 
        { "pico_id": child_id,
          "eci": new_channel{"id"},
          "dname": dname,
          "method": method
        };
    }
  }

  rule owner_new_owner_pico_with_code {
    select when owner new_owner_pico_with_code
    pre {
      owner_pico_id = event:attr("owner_pico_id");
    }
    every {
      engine:newChannel(owner_pico_id,"code query","secret") setting(code_query_channel);
      send_directive("new owner pico code query channel",
        event:attrs().put("eci",code_query_channel{"id"}));
    }
  }

Event information:child_deleted

This event will be sent by Wrangler to the Root Pico when one of it's direct child picos (that is, an owner pico) has been deleted. You, or anyone with access to your Root Pico can delete an owner pico in the "About" tab, by clicking on the link "del" beside the name of the owner pico in the list of Children. Your account management ruleset must react to this event by removing the owner pico from its directory. This is accomplished in the reference ruleset as follows.

  rule pico_child_deleted {
    select when information child_deleted
    pre {
      child_id = event:attr("id");
      owner_id = ent:owners.filter(function(v){v{"pico_id"}==child_id}).keys()[0];
    }
    if owner_id then noop();
    fired {
      clear ent:owners{owner_id};
    }
  }


Account ruleset

The account ruleset must react to these events, as described below:

  • owner:admin
  • owner:creation
  • owner:eci_provided
  • owner:authenticate
  • owner:code_presented
  • owner:new_password

The account ruleset may provide an event to securely store the owner-provided password. In the reference ruleset, this is owner:pwd_needs_encoding

In addition, it must share this function:

  • code

Event owner:admin

This event may be sent by your account management ruleset as part of its initialization. It establishes an initial password for the Root Pico, which you should manually change to secure your pico engine. The reference ruleset does this with a single rule.

  rule owner_admin {
    select when owner admin
    pre {
      txnId = event:attr("txnId");
      legit = (txnId == meta:txnId);
    }
    if legit then noop();
    fired {
      ent:owner_id := "Root";
      raise owner event "pwd_needs_encoding" attributes { "password": "toor" };
    }
  }

Event owner:creation

This event is sent by your account management ruleset after it has created the new owner pico and installed in it your account ruleset. It must record the owner's initial password (if that is the authentication method chosen by the owner), and may do other things. The reference ruleset does this with one rule.

  rule owner_creation {
    select when owner creation
    if ent:owner_id != "Root" then noop();
    fired {
      ent:owner_id := event:attr("owner_id");
      ent:method   := event:attr("method");
      raise owner event "pwd_needs_encoding" attributes { "password": event:attr("password") };
    }
  }

Event owner:eci_provided

The event is sent by your account management ruleset after it has provided the UI with the ECI to be used to contact the (as yet unauthenticated) owner pico. Your ruleset must establish the current code words, if this owner uses that method. It should record the nonce used to securely connect the authentication form to the next event (which will be either owner:authenticate (for the password mathod) or owner:code_provided). The reference ruleset does this with one rule.

  rule owner_eci_provided {
    select when owner eci_provided
    fired {
      ent:code := random:word() + "-" + random:word();
      ent:nonce := event:attr("nonce");
      schedule owner event "nonce_cleanup" at time:add(time:now(), {"minutes": 5}) setting(exp);
      ent:exp := exp;
    }
  }

Your ruleset may provide for clean-up by scheduling an event as shown.

Event owner:authenticate

This event is sent by the UI when the owner pushes the "Login" button on the password entry form, and has as attributes the nonce and the owner-supplied password. Your account ruleset should verify that the nonce attribute matches the saved value recorded when it reacted to the owner:eci_provided event. Your ruleset must send a directive, with a name of your choice, whose options must include pico_id and eci and may include other values. If the password or nonce are incorrect, your ruleset must not send a directive. The reference ruleset does this with one rule.

    rule owner_authenticate {
    select when owner authenticate
    if event:attr("nonce") == ent:nonce && passwordOK(event:attr("password"))
    then send_directive("success",{"pico_id":meta:picoId,"eci":meta:eci});
    fired {
      raise owner event "pwd_needs_encoding" attributes { "password": event:attr("password") }
        if pwd_needs_encoding();
    }
    finally {
      raise owner event "nonce_used";
    }
  }

The function passwordOK is defined in the reference ruleset.

    one_way_hash = function(password) {
      math:hash("sha256",ent:password{"salt"} + ":" + password)
    }
    passwordOK = function() {
      ent:password{"password"} == one_way_hash(password)
    }

The function pwd_needs_encoding is used in the reference ruleset to take advantage of a moment when we have in hand a correct owner password. We may wish to re-encrypt the password under certain conditions. In this example, we will encrypt the password if an earlier version of the ruleset had stored the owner-supplied password as a plain string.

    pwd_needs_encoding = function() {
      ent:password.typeof() == "String"
    }

This allows for graceful upgrading of password storage mechanisms. Flushing this ruleset in your pico engine which had an earlier version will result in the password being encrypted upon the next successful login attempt by each owner.

Event owner:code_presented

This event is sent by the UI when the owner pushes the "Login" button on the code words entry form, and has as attributes the nonce and the owner-supplied code words (code). Your account ruleset should verify that the nonce attribute matches the saved value recorded when it reacted to the owner:eci_provided event. If the code words are correct, your ruleset must send a directive, with a name of your choice, whose options must include pico_id and eci and may include other values. If the code words or nonce are incorrect, your ruleset must not send a directive. The reference ruleset does this with one rule.

  rule owner_match_code {
    select when owner code_presented
    if event:attr("code") == ent:code && event:attr("nonce") == ent:nonce then
      send_directive("success",{"pico_id":meta:picoId,"eci":meta:eci});
    always {
      raise owner event "nonce_used";
    }
  }

Event owner:new_password

This event is sent by the UI when the owner pushes the "New Password" button on the Change Password entry form in the owner pico's "About" tab, and has as attributes the current password and the owner-supplied new_password. Your account ruleset should verify the current password. Your ruleset must record the new password. The reference ruleset does this with one rule.

  rule owner_new_password {
    select when owner new_password
    if passwordOK(event:attr("password")) then noop();
    fired {
      ent:method := ent:method.defaultsTo("password");
      raise owner event "pwd_needs_encoding" attributes { "password": event:attr("new_password") };
    }
  }

Event owner:pwd_needs_encoding

This event is raised by every rule in the reference account ruleset which has access to the owner's password in clear text. Your account ruleset may have a similar rule.

  rule owner_pwd_needs_encoding {
    select when owner pwd_needs_encoding password re#^(.*)$# setting (password)
    pre {
      salt = random:uuid();
    }
    fired {
      ent:password := {};
      ent:password{"salt"} := salt;
      ent:password{"password"} := one_way_hash(password);
      ent:password{"last_encoding"} := time:now();
    }
  }

Function code

Your account ruleset must be able to provide the current code words (established when it reacted to the owner:eci_provided event) when requested by the owner, who uses the URL provided when he created his owner pico. The reference ruleset defines this function, and which it must also mention in the shares clause of its meta block.

    code = function() {
      ent:code || "code words expired"
    }

Technical notes

Besides the reference rulesets shown on this page, there are official rulesets, named io.picolabs.account_management and io.picolabs.owner_authentication, located in GitHub. Because of their location, they will already be registered in your node pico engine. You may choose to use these rulesets instead of creating your own. You may find it instructive to compare them with the reference rulesets, which they will eventually replace.

Throughout this documentation page, we have used "must, must not, may, and should" in the same sense in which they are used in IETF documents, as explained in RFC2119.

Cleanup of the nonce

You may use this pattern, from the account reference ruleset, to cleanup. This will ensure that the nonce and code words will not remain in the owner pico after authentication, or if authentication fails, or if they are not used after some amount of time has elapsed. When the reference ruleset reacts to the owner:eci_provided event, it schedules an owner:nonce_cleanup event for five minutes in the future. This allows your owner five minutes to suppy her password or code words to complete the login process. If she delays beyond that point she will have to start over by re-entering her owner id in the Login form. This is accomplished by these rules in the reference ruleset.

  rule owner_nonce_used {
    select when owner nonce_used
             or pico intent_to_orphan
    if ent:exp then schedule:remove(ent:exp);
    always {
      raise owner event "nonce_cleanup"
    }
  }
  rule owner_nonce_cleanup {
    select when owner nonce_cleanup
    always {
      clear ent:code;
      clear ent:nonce;
      clear ent:exp;
    }
  }

Notice that nonce cleanup will occur whenever the nonce has been used (successfully or otherwise), when the owner pico is about to be deleted, or when the five minutes have elapsed without either of those events happening. If applicable the scheduled event will be removed.


Copyright Picolabs | Licensed under Creative Commons.