Login With NFC In PHP MYSQL (It’s Possible!)

Yes, we have something called “WebNFC” in modern Javascript. It is possible to read/write NFC tags in web apps, and use it for user login. But take note, this is a “Master Coffee’s experiment”. The WebNFC API will only work in Android Chrome at the time of writing – You can check with CanIUse for the feature status.

 

CODE DOWNLOAD

I have released this under the MIT license, feel free to use it in your own project – Personal or commercial. Some form of credits will be nice though. 🙂

 

 

VIDEO TUTORIAL

 

PROJECT SETUP

 

 

PART 1) USER’S DATABASE

1-database.sql
-- (PART A) USERS TABLE
CREATE TABLE `users` (
  `user_id` bigint(20) NOT NULL,
  `user_email` varchar(255) NOT NULL,
  `user_password` varchar(255) NOT NULL,
  `nfc_hash` varchar(255) NULL,
  `nfc_create` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
ALTER TABLE `users`
  ADD PRIMARY KEY (`user_id`),
  ADD UNIQUE KEY `user_email` (`user_email`),
  ADD KEY `nfc_create` (`nfc_create`);
 
ALTER TABLE `users`
  MODIFY `user_id` bigint(20) NOT NULL AUTO_INCREMENT;
 
-- (PART B) DUMMY USER
INSERT INTO `users` (`user_id`, `user_email`, `user_password`, `nfc_hash`, `nfc_create`)
VALUES (NULL, '[email protected]', 'DOES-NOT-MATTER', null, null);

Let us start with a dummy user’s database, the first 3 fields should not be a surprise.

  • user_id Primary key and auto-increment.
  • user_email Email, set to unique to prevent duplicates.
  • user_password Login password. Not important for this demo.

Then, we add 2 more fields to cater for the NFC login mechanism.

  • nfc_hash A randomly generated hash, you can call this the “alternate login password.”
  • nfc_create A timestamp of when the NFC hash is created. Use this if you want to set an expiry on the login token.

 

 

PART 2) NFC LOGIN LIBRARY

2-lib.php
class Users {
  // (PART A) SETTINGS - CHANGE TO YOUR OWN!
  // (A1) DATABASE
  private $db_host = "localhost";
  private $db_name = "test";
  private $db_char = "utf8mb4";
  private $db_user = "root";
  private $db_pass = "";
  private $db_error = PDO::ERRMODE_EXCEPTION;
  private $db_fetch = PDO::FETCH_ASSOC;
 
  // (A2) NFC & JWT
  private $nfc_length = 12;
  private $jwt_secret = "SECRET-KEY";
  private $jwt_algo = "HS256";
 
  // (PART B) CONSTRUCTOR & DESTRUCTOR
  // (B1) CONNECT TO DATABASE
  public $error = null;
  private $pdo = null;
  private $stmt = null;
  function __construct () {
    $this->pdo = new PDO(
      "mysql:host=$this->db_host;dbname=$this->db_name;charset=$this->db_char",
      $this->db_user, $this->db_pass, [
        PDO::ATTR_ERRMODE => $this->db_error,
        PDO::ATTR_DEFAULT_FETCH_MODE => $this->db_fetch
      ]);
    }
 
  // (B2) CLOSE DATABASE CONNECTION
  function __destruct () {
    if ($this->stmt !== null) { $this->stmt = null; }
    if ($this->pdo !== null) { $this->pdo = null; }
  }
 
  // (PART C) HELPER - RUN SQL QUERY
  function query ($sql, $data=null) : void {
    $this->stmt = $this->pdo->prepare($sql);
    $this->stmt->execute($data);
  }
 
  // (PART D) GET USER BY ID/EMAIL
  function get ($id) {
    $this->query(sprintf(
      "SELECT * FROM `users` WHERE users.`user_%s`=?",
      is_numeric($id) ? "id" : "email"
    ), [$id]);
    return $this->stmt->fetch();
  }
 
  // (PART E) CREATE NFC LOGIN FOR USER
  function createNFC ($id) {
    // (E1) CHECK IF USER REGISTERED
    $user = $this->get($id);
    if (!is_array($user)) {
      $this->error = "User is not registered.";
      return false;
    }
 
    // (E2) GENERATE HASH
    $token = substr(preg_replace("/[^A-Za-z0-9]/", "", base64_encode(random_bytes($this->nfc_length * 2))), 0, $this->nfc_length);
    $this->query(
      "UPDATE `users` SET `nfc_hash`=?, `nfc_create`=? WHERE `user_id`=?",
      [password_hash($token, PASSWORD_DEFAULT), date("Y-m-d H:i:s"), $id]
    );
 
    // (E3) ENCODE & RETURN TOKEN
    // * THIS TOKEN IS TO BE WRITTEN INTO AN NFC TAG
    require __DIR__ . "/vendor/autoload.php";
    return Firebase\JWT\JWT::encode([$id, $token], $this->jwt_secret, $this->jwt_algo);
  }
 
  // (PART F) USER LOGIN WITH NFC TOKEN
  function loginNFC ($token) {
    // (F1) DECODE THE TOKEN
    $valid = true;
    try {
      require __DIR__ . "/vendor/autoload.php";
      $token = Firebase\JWT\JWT::decode(
        $token, new Firebase\JWT\Key($this->jwt_secret, $this->jwt_algo)
      );
      $valid = is_object($token);
      if ($valid) {
        $token = (array) $token;
        $valid = count($token)==2;
      }
    } catch (Exception $e) { $valid = false; }
 
    // (F2) VERIFY THE TOKEN
    if ($valid) {
      $user = $this->get($token[0]);
      $valid = (is_array($user) && password_verify($token[1], $user["nfc_hash"]));
    }
 
    // (F3) VALID LOGIN - SESSION START
    if ($valid) {
      unset($user["user_password"]);
      unset($user["nfc_hash"]);
      unset($user["nfc_create"]);
      $_SESSION["user"] = $user;
      return true;
    }
 
    // (F4) INVALID LOGIN
    $this->error = "Invalid login token";
    return false;
  }
}
 
// (PART G) CREATE USER OBJECT
$USR = new Users();

Keep calm and drink coffee.

  1. A whole bunch of settings. Change the database settings and set your own secret key.
  2. When new Users() is created, the constructor will connect to the database. The destructor closes the connection.
  3. query() A helper function to run an SQL query.
  4. get() Get a user by ID or email.
  5. createNFC() Create an NFC login token for a given user ID/email. This will return a token, that is to be written to an NFC tag.
  6. loginNFC() The user scans the NFC tag, this will verify the token against the database and login.

Note – I am using the traditional $_SESSION for the user session here. Feel free to change this if you are using other means to keep the user session.

 

 

PART 3) CREATE NFC LOGIN TAG

3A) CREATE NFC LOGIN TAG PAGE

3a-create.php
<?php
// (PART A) GENERATE NFC TOKEN FOR "JON DOE"
require "2-lib.php";
$token = $USR->createNFC(1); ?>
<input type="hidden" id="token" value="<?=$token?>">
 
<!-- (PART B) NFC WRITER STATUS -->
<div id="status">LOADING...</div>

Very simple page to create an NFC login tag.

  1. Use the library to create the login tag, put it into a hidden field.
  2. Just a “status display”.

 

 

3B) JAVASCRIPT WRITE NFC TAG

3b-create.js
window.addEventListener("DOMContentLoaded", () => {
  // (PART A) GET HTML ELEMENTS
  const htoken = document.getElementById("token"), 
        hstat = document.getElementById("status");
 
  // (PART B) CHECK IF WEBNFC IS SUPPORTED
  if (!("NDEFReader" in window)) {
    hstat.innerHTML = "WebNFC is not supported on this browser!";
    return;
  }
 
  // (PART C) START NFC WRITER
  // (C1) WEBNFC OBJECTS
  const ncon = new AbortController(),
        ndef = new NDEFReader();
 
  // (C2) START WRITING
  ndef.write(htoken.value, { signal: ncon.signal })
 
  // (C3) ON WRITE SUCCESS
  .then(() => {
    ncon.abort();
    hstat.innerHTML = "Login token created!";
  })
 
  // (C4) ON WRITE FAILURE
  .catch(err => {
    ncon.abort();
    hstat.innerHTML = err.msg;
  });
 
  // (C5) READY!
  hstat.innerHTML = "Ready - Tap NFC tag to write.";
});
  1. On window load, we get the hidden token and “status display”.
  2. Stop if the browser does not support WebNFC.
  3. Create the NFC login token.

 

 

PART 4) LOGIN WITH NFC TOKEN

4A) LOGIN WITH NFC TAG PAGE

4a-login.html
<div id="status">LOADING...</div>

Well, pretty much just a “status display”.

 

4B) JAVASCRIPT SCAN & LOGIN WITH NFC

4b-login.js
window.addEventListener("DOMContentLoaded", () => {
  // (PART A) GET HTML ELEMENTS
  const hstat = document.getElementById("status");
 
  // (PART B) CHECK IF WEBNFC IS SUPPORTED
  if (!("NDEFReader" in window)) {
    hstat.innerHTML = "WebNFC is not supported on this browser!";
    return;
  }
 
  // (PART C) START NFC READER
  // (C1) START READING LOGIN TOKEN
  const ncon = new AbortController(),
        ndef = new NDEFReader();
 
  // (C2) ON SUCCESSFUL READ - SEND TOKEN TO SERVER
  ndef.onreading = evt => {
    // (C2-1) STOP SCANNING
    ncon.abort();
    hstat.innerHTML = "LOGGING IN...";
 
    // (C2-2) GET NFC TOKEN
    let data = new FormData();
        decoder = new TextDecoder();
    data.append("token", decoder.decode(evt.message.records[0].data));
 
    // (C2-3) SEND TOKEN TO SERVER
    fetch("4c-login.php", { method: "POST", body: data })
    .then(res => res.text())
    .then(res => {
      hstat.innerHTML = res;
      // DO WHATEVER YOU NEED ON SUCCESS - RELOAD, REDIRECT, ETC...
    })
    .catch(err => hstat.innerHTML = err.msg);
  };
 
  // (C3) ON READ FAILURE
  ndef.onreadingerror = (err => {
    ncon.abort();
    hstat.innerHTML = err.msg;
  });
 
  // (C4) START SCANNING
  ndef.scan({ signal: ncon.signal })
  .catch(err => {
    ncon.abort();
    hstat.innerHTML = err.msg;
  });
 
  // (C5) READY!
  hstat.innerHTML = "Ready - Tap NFC tag to login.";
});
  1. Get the status display on window load.
  2. Once again, stop if WebNFC is not supported.
  3. Process the login when the user scans the NFC tag. Not going to explain line-by-line but a quick trace:
    • (C4) Start scanning.
    • (C2) On successful scan, decode the NFC token.
    • (C2-3) Send the token to server for verification.

 

 

4C) LOGIN AJAX HANDLER

4c-login.php
<?php
require "2-lib.php";
if ($USR->loginNFC($_POST["token"])) {
  echo "OK - ";
  print_r($_SESSION);
} else {
  echo "ERROR - " . $USR->error;
}

Lastly, this will handle the NFC login – Simply use the library to verify and show “OK” or “ERROR”.

 

 

THE END – NOT THE END

That’s all for this quick sharing. But take note, this is only a working example. A lot of “polishing” has to be done if you want to adapt it into your project:

  • Secure the “generate NFC token” page, only allow administrators to access.
  • As above – Set an expiry on the NFC login token for additional security.
  • Add another function to immediately cancel a login token. Also let users cancel their own token if they lose it.
  • Complete the login page.