blog:2020:0701_homectrl_authentication_handling

HomeCtrl: a touch of security

In our previous article we discussed the initial construction of the HomeCtrl project. The result of that is that currently, anyone sending a post request to the correct URL mentioned in that other article could basically trigger the opening of our entrance gate. [OOooopss…]

Clearly, from a security perspective, that is not so good ;-) So I was thinking we could try to do something about it here…

Sending a user and password as part of the post data

In fact, there is no reason to make this too complex: what we really want is to be able to identify who is sending the request to activate the entrance gate. To achieve this result it would be enough to send a user name and password as part of the post request data, and compare those elements with an array we keep inside the server process.

⇒ Nothing too fancy, no need for a database or anything like that here. Let's give this a try and see how it works!

As an authentication scheme I will (for now) simply concatenate a user name with its password. Since this POST data is sent over https, this will be secured already. As a result, now I need this kind of request, otherwise the authentication will fail:

curl -H "Content-Type: application/json" -d '{"device": "entrance", "action":"trigger", "auth":"manu_5678"}' -X POST https://api.nervtech.org/homectrl/trigger
[Side note lol] To check how long a process has been running on linux, one can first retrieve the process ID and then use the command ls -ld /proc/<pid> to find out when the process was started.
  • Adding a new page to enter the user credentials on the ionic app:
    nv_cmd
    ionic generate page login
  • To store our username/password locally on the user device, we can use the ionic storage component
    • So we install the ionic storage package:
      npm install --save @ionic/storage
    • Then we inject the module in the app root module:
      @NgModule({
        declarations: [AppComponent],
        entryComponents: [],
        imports: [BrowserModule, HttpClientModule, IonicModule.forRoot(), IonicStorageModule.forRoot(), AppRoutingModule],
        providers: [
          StatusBar,
          SplashScreen,
          { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
        ],
        bootstrap: [AppComponent]
      })
  • And then we use it on our login page to save/restore the user provided values:
    import { Component, OnInit } from '@angular/core';
    import { Router } from '@angular/router';
    import { Storage } from '@ionic/storage';
    
    @Component({
      selector: 'app-login',
      templateUrl: './login.page.html',
      styleUrls: ['./login.page.scss'],
    })
    export class LoginPage implements OnInit {
    
      protected username: string = "";
      protected password: string = "";
    
      constructor(public router: Router, private sto: Storage) { }
    
      public gotoHome()
      {
        this.router.navigate(['/home'])
      }
    
      public onUsernameUpdated(val)
      {
        // console.log("Username updated to: '"+val+"'")
        this.username = val
        this.sto.set("username", val)
      }
    
      public onPasswordUpdated(val)
      {
        // console.log("Password updated to: '"+val+"'")
        this.password = val
        this.sto.set("password", val)
      }
    
      ngOnInit() {
        // Assign the saved username and password:
        this.sto.get('username').then((val) => {
          // console.log("Using saved username: "+val)
          this.username = val
        });
    
        this.sto.get('password').then((val) => {
          // console.log("Using saved password: "+val)
          this.password = val
        });
      }
    
    }
    
  • Finally, I will now also use the stored username/password values when sending an activation request from the home page:
      public activateEntranceGate()
      {
        console.log("Requesting entrance gate activation...");
        
        if (this.toast != null) {
          this.toast.dismiss();
          this.toast = null;
        }
    
        this.requestSent = true;
    
        // When about to send a request we should retrieve the saved username and password:
        let username:string = "";
        let password:string = "";
        this.sto.get("username")
        .then((val) => {
          username = val
          return this.sto.get("password")
        })
        .then((val) => {
          password = val
    
          // let addr = "http://192.168.1.2/trigger"
          let addr = "https://api.nervtech.org/homectrl/trigger"
          let body = {device: 'entrance', 
                      action: 'trigger',
                      auth: username+"_"+password}
    
          this.http.post<any>(addr, body).subscribe(data => {
            console.log("Received reply: "+JSON.stringify(data))
            
            this.requestSent = false;
      
            if(data['status'] == 'OK') {
              this.presentToast("Gate activated successfully.", "success");
            }
            else {
              this.presentToast(data['message'], "warning");
            }
      
          })
       
        })
    
      }

⇒ And after rebuilding/re-installing the android application, this seems to work just fine: I can enter my credentials on the login page, and they are used as expected on the home page. If I close and restart the app, then the previously entered values are retrieved from [some form of] local storage, so we don't have to enter them again. All good!

On the server side, I also added a couple of helper functions in the main script:

def printMsg(msg):
  print(msg, file=sys.stderr)
  
def readConfigFile():
  cfgFile=os.path.join(dirname, "homectrl.json")
  printMsg("Reading config file: %s" % cfgFile)
  with open(cfgFile) as json_file:
      data = json.load(json_file)
  
  return data

def getAuthenticatedUser(cfg, auth):
  users = cfg['users']

  for uname in users.keys():
    if auth == "%s_%s" % (uname, users[uname]):
      return uname
  
  return None

And then, I use those functions to check if the activation requests I reveice are from properly authenticated users:

@app.route("/trigger", methods = ['POST'])
def on_trigger():
  if request.method == 'POST':
    # data = request.form # a multidict containing POST data
    data = request.json
    printMsg("Received post data: %s" % data)
    dev = data.get('device')
    act = data.get('action')
    auth = data.get('auth')

    # Here we should also read the json config file:
    cfg = readConfigFile()

    # check if we have one authentization code corresponding to the provided auth:
    user = getAuthenticatedUser(cfg, auth)

    if user == None:
      printMsg("Cannot authenticate user with auth: %s" % auth)
      return jsonify(status = "Error", message = "Cannot authenticate user.")

    # dev = data['device']
    # act = data['action']
    if dev == 'entrance' and act == "trigger":
      now = datetime.now()
      printMsg('%s: Entrance gate activation requested by %s' % (now, user))
      triggerGate()
      return jsonify(status = "OK")
    else:
      return jsonify(status = "Error", message = "Invalid device/action: dev=%s, act=%s" % (dev, act))
  else:
    printMsg("not supported request method: %s" % request.method)
    return jsonify(status = "Error", message = "Invalid request method %s" % request.method)

The very nice thing here is that the user credentials are just stored in a JSON file, with for instance the following content:

{
    "users": {
        "flo": "1234",
        "manu": "5678"
    }
}

And that file is re-read for each request we receive. So from there, it will be very easy to add/remove users dynamically since I have a direct access to that file with my samba shared folder ;-).

Of course this means we are not handling any kind of “real password” here since I will be the one defining them for each user, but hey, we really don't need more than that, and I definitely want to know who has the right to open my home entrance gate at any time anyway.
  • blog/2020/0701_homectrl_authentication_handling.txt
  • Last modified: 2020/07/10 12:11
  • by 127.0.0.1