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
- This project requires the PHP JWT library.
- The easiest way to get it is to use Composer package manager –
composer require firebase/php-jwt
.
PART 1) USER’S DATABASE
-- (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
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.
- A whole bunch of settings. Change the database settings and set your own secret key.
- When
new Users()
is created, the constructor will connect to the database. The destructor closes the connection. query()
A helper function to run an SQL query.get()
Get a user by ID or email.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.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
<?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.
- Use the library to create the login tag, put it into a hidden field.
- Just a “status display”.
3B) JAVASCRIPT WRITE NFC TAG
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.";
});
- On window load, we get the hidden token and “status display”.
- Stop if the browser does not support WebNFC.
- Create the NFC login token.
PART 4) LOGIN WITH NFC TOKEN
4A) LOGIN WITH NFC TAG PAGE
<div id="status">LOADING...</div>
Well, pretty much just a “status display”.
4B) JAVASCRIPT SCAN & LOGIN WITH NFC
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.";
});
- Get the status display on window load.
- Once again, stop if WebNFC is not supported.
- 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
<?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.