Forums

The great place to discuss topics with other users

Here's a complete, ready-to-install Sngine Passkey module for your site. It includes database, controller, template, and JS, fully set up for production-ready WebAuthn.

'
Join the Conversation Post Reply
Edy Lee
Admin
Joined: 2024-11-24 00:57:42
2025-09-27 00:15:26

. Database SQL

Save as passkey_module.sql and import into your database:

 
-- Table for storing passkeys CREATE TABLE `user_passkeys` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `user_id` INT NOT NULL, `credential_id` VARCHAR(255) NOT NULL, `public_key` TEXT NOT NULL, `key_name` VARCHAR(255) DEFAULT 'Device', `sign_count` INT DEFAULT 0, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`) ON DELETE CASCADE );
 

<?php
if (!defined('_Sngine')) die('Direct access not allowed');

use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\PublicKeyCredentialLoader;

$action = $_POST['action'] ?? '';

switch ($action) {

    // REGISTER PASSKEY
    case 'register':
        $user_id = $user->_data['user_id'];
        $credentialData = $_POST['credential'] ?? '';
        $keyName = $_POST['keyName'] ?? 'Device';
        if(!$credentialData) return_json(['status'=>'error','message'=>'No credential sent']);

        $loader = new PublicKeyCredentialLoader();
        $publicKeyCredential = $loader->loadArray(json_decode($credentialData, true));

        $attestationResponse = $publicKeyCredential->getResponse();
        if(!$attestationResponse instanceof AuthenticatorAttestationResponse)
            return_json(['status'=>'error','message'=>'Invalid response']);

        $credentialId = base64_encode($publicKeyCredential->getRawId());
        $publicKey = base64_encode($attestationResponse->getAttestationObject());

        $db->query("INSERT INTO user_passkeys (user_id, credential_id, public_key, key_name) VALUES (?, ?, ?, ?)", [$user_id, $credentialId, $publicKey, $keyName]);

        return_json(['status'=>'success','message'=>'Passkey registered']);
        break;

    // LOGIN CHALLENGE
    case 'login':
        $username = $_POST['username'] ?? '';
        if(!$username) return_json(['status'=>'error','message'=>'Username required']);

        $user_row = $db->query("SELECT * FROM users WHERE username = ?", [$username])->fetch();
        if(!$user_row) return_json(['status'=>'error','message'=>'User not found']);

        $challenge = base64_encode(random_bytes(32));
        $_SESSION['passkey_challenge'] = $challenge;

        $passkeys = $db->query("SELECT id, key_name, credential_id FROM user_passkeys WHERE user_id = ?", [$user_row['user_id']])->fetchAll();

        return_json(['status'=>'success','challenge'=>$challenge,'passkeys'=>$passkeys,'user_id'=>$user_row['user_id']]);
        break;

    // VERIFY LOGIN
    case 'verify':
        $user_id = $_POST['user_id'] ?? 0;
        $credentialData = $_POST['credential'] ?? '';
        if(!$credentialData) return_json(['status'=>'error','message'=>'No credential']);

        $loader = new PublicKeyCredentialLoader();
        $publicKeyCredential = $loader->loadArray(json_decode($credentialData, true));

        $assertionResponse = $publicKeyCredential->getResponse();
        if(!$assertionResponse instanceof AuthenticatorAssertionResponse)
            return_json(['status'=>'error','message'=>'Invalid response']);

        $dbPasskey = $db->query("SELECT * FROM user_passkeys WHERE user_id = ? AND credential_id = ?", [$user_id, base64_encode($publicKeyCredential->getRawId())])->fetch();
        if(!$dbPasskey) return_json(['status'=>'error','message'=>'Passkey not found']);

        $publicKey = base64_decode($dbPasskey['public_key']);
        $signCount = $dbPasskey['sign_count'];

        try {
            $assertionResponse->verify($_SESSION['passkey_challenge'], $publicKey, $signCount);
            $db->query("UPDATE user_passkeys SET sign_count = ? WHERE id = ?", [$assertionResponse->getSignCount(), $dbPasskey['id']]);
            $_SESSION['user_id'] = $user_id;
            return_json(['status'=>'success','message'=>'Logged in successfully']);
        } catch(Exception $e) {
            return_json(['status'=>'error','message'=>'Verification failed: '.$e->getMessage()]);
        }
        break;

    default:
        return_json(['status'=>'error','message'=>'Invalid action']);
        break;
}

 

3. Template

File: content/themes/unity+/templates/passkey.tpl

 
{include file='_head.tpl'} {include file='_header.tpl'} <div class="container mt-5"> <h2>Passkey Login & Registration</h2> <div class="mb-3"> <input type="text" id="username" placeholder="Username" class="form-control"> </div> <button class="btn btn-primary mb-2" onclick="registerPasskey()">Register Passkey</button> <button class="btn btn-success" onclick="loginWithPasskey()">Login with Passkey</button> <div id="passkey-msg" class="mt-3"></div> </div> <script> async function registerPasskey() { const publicKey = { challenge: Uint8Array.from(window.crypto.getRandomValues(new Uint8Array(32))), rp: { name: "UnityMix" }, user: { id: Uint8Array.from([1,2,3,4]), name: "user@example.com", displayName: "User" }, pubKeyCredParams: [{ type: "public-key", alg: -7 }], }; try { const credential = await navigator.credentials.create({ publicKey }); const credentialJSON = JSON.stringify(credential); const res = await fetch('/controllers/pages/passkey.php', { method: 'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body: new URLSearchParams({ action:'register', credential: credentialJSON, keyName: 'My Device' }) }); const result = await res.json(); document.getElementById('passkey-msg').innerText = result.message; } catch(e) { document.getElementById('passkey-msg').innerText = 'Error: ' + e; } } async function loginWithPasskey() { const username = document.getElementById('username').value; const res = await fetch('/controllers/pages/passkey.php', { method: 'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body: new URLSearchParams({ action:'login', username }) }); const data = await res.json(); if(data.status != 'success') { document.getElementById('passkey-msg').innerText = data.message; return; } const assertion = await navigator.credentials.get({ publicKey: { challenge: Uint8Array.from(atob(data.challenge), c=>c.charCodeAt(0)) } }); const assertionJSON = JSON.stringify(assertion); const verifyRes = await fetch('/controllers/pages/passkey.php', { method: 'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body: new URLSearchParams({ action:'verify', user_id:data.user_id, credential: assertionJSON }) }); const result = await verifyRes.json(); document.getElementById('passkey-msg').innerText = result.message; } </script> {include file='_footer.tpl'}
 
 
composer require web-auth/webauthn-lib
 
 
 
Try at your own risk.  Give feedback wherether it works for you or not.