Initial implementation
This commit is contained in:
commit
4872d55ed6
55
README.md
Normal file
55
README.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# Implementing passkey
|
||||||
|
|
||||||
|
## Add passkey to user update form
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
<!-- Passkeys -->
|
||||||
|
<script type='text/javascript' src='{{ asset('/passkey/passkey.js') }}'></script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#passkey').on('click',function(item) {
|
||||||
|
if (passkey_debug)
|
||||||
|
console.log('Passkey: Create Click');
|
||||||
|
|
||||||
|
// Availability of `window.PublicKeyCredential` means WebAuthn is usable.
|
||||||
|
// `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable.
|
||||||
|
// `sConditionalMediationAvailable` means the feature detection is usable.
|
||||||
|
if (window.PublicKeyCredential &&
|
||||||
|
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
|
||||||
|
PublicKeyCredential.isConditionalMediationAvailable) {
|
||||||
|
// Check if user verifying platform authenticator is available.
|
||||||
|
Promise.all([
|
||||||
|
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),
|
||||||
|
PublicKeyCredential.isConditionalMediationAvailable(),
|
||||||
|
]).then(results => {
|
||||||
|
if (passkey_debug)
|
||||||
|
console.log('Passkey: Browser Supported');
|
||||||
|
|
||||||
|
if (results.every(r => r === true)) {
|
||||||
|
passkey_register('{{ csrf_token() }}',$(this),'bi-key','btn-primary','{{ $o->passkey ? 'btn-primary' : 'btn-outline-primary' }}');
|
||||||
|
} else {
|
||||||
|
alert('It seems that passkey is NOT supported by your browse (B)');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
alert('It seems that passkey is NOT supported by your browser (A)');
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Enable passkey login
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
<!-- Passkeys -->
|
||||||
|
<script type='text/javascript' src='{{ asset('/passkey/passkey.js') }}'></script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
passkey_check('{{ csrf_token() }}','{{ back()->getTargetUrl() }}');
|
||||||
|
</script>
|
||||||
|
```
|
33
composer.json
Normal file
33
composer.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "leenooks/passkey",
|
||||||
|
"description": "Leenooks Laravel Passkey Implementation",
|
||||||
|
"keywords": [
|
||||||
|
"laravel",
|
||||||
|
"dege",
|
||||||
|
"passkey"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Deon George",
|
||||||
|
"email": "deon@dege.au"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"lbuchs/webauthn": "^2.1"
|
||||||
|
},
|
||||||
|
"require-dev": {},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Leenooks\\Passkey\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Leenooks\\Passkey\\PasskeyServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "dev"
|
||||||
|
}
|
232
src/Controllers/PasskeyController.php
Normal file
232
src/Controllers/PasskeyController.php
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Leenooks\Passkey\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use lbuchs\WebAuthn\WebAuthn;
|
||||||
|
use lbuchs\WebAuthn\WebAuthnException;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class PasskeyController extends Controller
|
||||||
|
{
|
||||||
|
const LOGKEY = 'PK-';
|
||||||
|
|
||||||
|
private array $formats = [
|
||||||
|
'none',
|
||||||
|
//'android-key',
|
||||||
|
//'android-safetynet',
|
||||||
|
//'apple',
|
||||||
|
//'fido-u2f',
|
||||||
|
//'packed',
|
||||||
|
//'tpm',
|
||||||
|
];
|
||||||
|
|
||||||
|
private int $timeout = 60*4;
|
||||||
|
|
||||||
|
// So users can login without providing their ID
|
||||||
|
private bool $requireResidentKey = TRUE;
|
||||||
|
private ?string $userVerification = 'preferred';
|
||||||
|
private ?bool $crossPlatformAttachment;
|
||||||
|
private array $excludeCredentials = [];
|
||||||
|
|
||||||
|
private bool $typeInt = TRUE;
|
||||||
|
private bool $typeHyb = FALSE;
|
||||||
|
private bool $typeUsb = FALSE;
|
||||||
|
private bool $typeNfc = FALSE;
|
||||||
|
private bool $typeBle = FALSE;
|
||||||
|
|
||||||
|
private string $userId;
|
||||||
|
private string $userName;
|
||||||
|
private string $userDisplayName;
|
||||||
|
|
||||||
|
private WebAuthn $webauthn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the passkey challenge arguments
|
||||||
|
*
|
||||||
|
* @return \stdClass
|
||||||
|
* @throws \Psr\Container\ContainerExceptionInterface
|
||||||
|
* @throws \Psr\Container\NotFoundExceptionInterface
|
||||||
|
*/
|
||||||
|
public function args(): \stdClass
|
||||||
|
{
|
||||||
|
$this->init();
|
||||||
|
$ids = [];
|
||||||
|
|
||||||
|
$getArgs = $this->webauthn->getGetArgs(
|
||||||
|
$ids,
|
||||||
|
$this->timeout,
|
||||||
|
$this->typeUsb,
|
||||||
|
$this->typeNfc,
|
||||||
|
$this->typeBle,
|
||||||
|
$this->typeHyb,
|
||||||
|
$this->typeInt,
|
||||||
|
$this->userVerification
|
||||||
|
);
|
||||||
|
|
||||||
|
session()->put('challenge',$this->webauthn->getChallenge());
|
||||||
|
|
||||||
|
return $getArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function takes a browsers generated passkey, checks that its valid and registers it.
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @return array
|
||||||
|
* @throws \Psr\Container\ContainerExceptionInterface
|
||||||
|
* @throws \Psr\Container\NotFoundExceptionInterface
|
||||||
|
* @throws \lbuchs\WebAuthn\WebAuthnException
|
||||||
|
*/
|
||||||
|
public function check(Request $request): array
|
||||||
|
{
|
||||||
|
Log::debug(sprintf('%s:- Check',self::LOGKEY),['post'=>$request->post()]);
|
||||||
|
|
||||||
|
$this->init();
|
||||||
|
|
||||||
|
$challenge = session()->pull('challenge');
|
||||||
|
Log::debug(sprintf('%s:- Challenge Retrieved [%s]',self::LOGKEY,$challenge));
|
||||||
|
|
||||||
|
// processCreate returns data to be stored for future logins.
|
||||||
|
$data = $this->webauthn->processCreate(
|
||||||
|
base64_decode($request->clientDataJSON),
|
||||||
|
base64_decode($request->attestationObject),
|
||||||
|
$challenge,
|
||||||
|
$this->userVerification === 'required',
|
||||||
|
TRUE,
|
||||||
|
FALSE,
|
||||||
|
);
|
||||||
|
Log::debug(sprintf('%s:- process Create Data',self::LOGKEY),['data'=>serialize($data)]);
|
||||||
|
|
||||||
|
$uo = Auth::user();
|
||||||
|
$uo->passkey = [
|
||||||
|
'id' => base64_encode($data->credentialId),
|
||||||
|
'pk' => $data->credentialPublicKey,
|
||||||
|
];
|
||||||
|
$uo->save();
|
||||||
|
|
||||||
|
if ($data->rootValid === FALSE)
|
||||||
|
Log::alert(sprintf('%s:- registration ok, but certificate does not match any of the selected root ca.',self::LOGKEY));
|
||||||
|
|
||||||
|
Log::debug(sprintf('%s:= Passkey [%s] created for [%d]',self::LOGKEY,$uo->passkey['id'],$uo->id));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'msg' => 'Registration Successful',
|
||||||
|
'success' => TRUE,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function init()
|
||||||
|
{
|
||||||
|
Log::debug(sprintf('%s:- Init for [%d]',self::LOGKEY,Auth::id()));
|
||||||
|
|
||||||
|
$this->userId = sprintf('%08x',Auth::id());
|
||||||
|
$this->userName = Auth::id() ? Auth::user()->email : '';
|
||||||
|
$this->userDisplayName = Auth::id() ? preg_replace('/[^0-9a-z öüäéèàÖÜÄÉÈÀÂÊÎÔÛâêîôû]/i','',Auth::user()->name) : '';
|
||||||
|
|
||||||
|
// cross-platform: true, if type internal is not allowed
|
||||||
|
// false, if only internal is allowed
|
||||||
|
// null, if internal and cross-platform is allowed
|
||||||
|
if (($this->typeUsb || $this->typeNfc || $this->typeBle || $this->typeHyb) && (! $this->typeInt)) {
|
||||||
|
$this->crossPlatformAttachment = TRUE;
|
||||||
|
|
||||||
|
} elseif ((! $this->typeUsb) && (! $this->typeNfc) && (! $this->typeBle) && (! $this->typeHyb) && $this->typeInt) {
|
||||||
|
$this->crossPlatformAttachment = FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->webauthn = new WebAuthn('WebAuthn Library',request()->getHttpHost(),$this->formats);
|
||||||
|
}
|
||||||
|
|
||||||
|
// processGet
|
||||||
|
public function process(Request $request): array
|
||||||
|
{
|
||||||
|
Log::debug(sprintf('%s:- Process',self::LOGKEY),['post'=>$request->post()]);
|
||||||
|
|
||||||
|
$this->init();
|
||||||
|
|
||||||
|
$userHandle = base64_decode($request->userHandle);
|
||||||
|
$challenge = session()->pull('challenge');
|
||||||
|
|
||||||
|
Log::debug(sprintf('%s:- ID [%s], Handle [%s] (%s)',self::LOGKEY,$request->id,bin2hex($userHandle),$request->userHandle));
|
||||||
|
|
||||||
|
$uo = User::select(['id','email','passkey'])
|
||||||
|
->active()
|
||||||
|
->where('passkey->id',$request->id)
|
||||||
|
->single();
|
||||||
|
|
||||||
|
if (! $uo)
|
||||||
|
return [
|
||||||
|
'msg' => 'Passkey Not Found',
|
||||||
|
'success' => FALSE,
|
||||||
|
];
|
||||||
|
|
||||||
|
// if we have resident key, we have to verify that the userHandle is the provided userId at registration
|
||||||
|
if ($this->requireResidentKey && ($userHandle !== hex2bin(sprintf('%08s',$uo->id))))
|
||||||
|
throw new \Exception(sprintf('%s:! userId doesnt match (is [%s] but expect [%s])',self::LOGKEY,bin2hex($userHandle),$uo->id));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// process the get request. throws WebAuthnException if it fails
|
||||||
|
$this->webauthn->processGet(
|
||||||
|
base64_decode($request->clientDataJSON),
|
||||||
|
base64_decode($request->authenticatorData),
|
||||||
|
base64_decode($request->signature),
|
||||||
|
$uo->passkey['pk'],
|
||||||
|
$challenge,
|
||||||
|
NULL,
|
||||||
|
$this->userVerification === 'required'
|
||||||
|
);
|
||||||
|
|
||||||
|
Log::debug(sprintf('%s:= User logged in [%d]',self::LOGKEY,$uo->id));
|
||||||
|
|
||||||
|
Auth::login($uo);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'msg' => 'Login Successful',
|
||||||
|
'success' => TRUE,
|
||||||
|
];
|
||||||
|
|
||||||
|
} catch (WebAuthnException $e) {
|
||||||
|
Log::debug(sprintf('%s:= Passkey Failed for [%d] with msg [%s]',self::LOGKEY,$uo->id,$e->getMessage()));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'msg' => $e->getMessage(),
|
||||||
|
'success' => FALSE,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start User Enrollment registration
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @return \stdClass|void
|
||||||
|
* @throws \lbuchs\WebAuthn\WebAuthnException
|
||||||
|
*/
|
||||||
|
public function register(Request $request)
|
||||||
|
{
|
||||||
|
Log::debug(sprintf('%s:- Register for user [%d]',self::LOGKEY,Auth::id()),['request'=>$request]);
|
||||||
|
|
||||||
|
$this->init();
|
||||||
|
|
||||||
|
$createArgs = $this->webauthn->getCreateArgs(
|
||||||
|
hex2bin($this->userId),
|
||||||
|
$this->userName,
|
||||||
|
$this->userDisplayName,
|
||||||
|
$this->timeout,
|
||||||
|
$this->requireResidentKey,
|
||||||
|
$this->userVerification,
|
||||||
|
$this->crossPlatformAttachment,
|
||||||
|
$this->excludeCredentials,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save challenge to session. you have to deliver it to get() later.
|
||||||
|
session()->put('challenge',$x=$this->webauthn->getChallenge());
|
||||||
|
Log::debug(sprintf('%s:- Challenge Stored [%s]',self::LOGKEY,$x));
|
||||||
|
|
||||||
|
return $createArgs;
|
||||||
|
}
|
||||||
|
}
|
23
src/PasskeyServiceProvider.php
Normal file
23
src/PasskeyServiceProvider.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Leenooks\Passkey;
|
||||||
|
|
||||||
|
use Illuminate\Routing\Router;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides passkey integration for laravel
|
||||||
|
*/
|
||||||
|
class PasskeyServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Bootstrap the application services.
|
||||||
|
*
|
||||||
|
* @param Router $router
|
||||||
|
*/
|
||||||
|
public function boot(Router $router)
|
||||||
|
{
|
||||||
|
$this->loadRoutesFrom(__DIR__.'/routes.php');
|
||||||
|
$this->loadMigrationsFrom(__DIR__.'/database/migrations');
|
||||||
|
}
|
||||||
|
}
|
28
src/database/migrations/2024_04_22_223358_user_passkey.php
Normal file
28
src/database/migrations/2024_04_22_223358_user_passkey.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->jsonb('passkey')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['passkey']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
225
src/public/js/passkey.js
Normal file
225
src/public/js/passkey.js
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
/*
|
||||||
|
* Passkey Implementation
|
||||||
|
*/
|
||||||
|
let passkey_debug = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a ArrayBuffer to Base64
|
||||||
|
* @param {ArrayBuffer} buffer
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
function arrayBufferToBase64(buffer) {
|
||||||
|
let binary = '';
|
||||||
|
let bytes = new Uint8Array(buffer);
|
||||||
|
let len = bytes.byteLength;
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
binary += String.fromCharCode( bytes[ i ] );
|
||||||
|
}
|
||||||
|
return window.btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert RFC 1342-like base64 strings to array buffer
|
||||||
|
* @param {mixed} obj
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
function recursiveBase64StrToArrayBuffer(obj) {
|
||||||
|
let prefix = '=?BINARY?B?';
|
||||||
|
let suffix = '?=';
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
for (let key in obj) {
|
||||||
|
if (typeof obj[key] === 'string') {
|
||||||
|
let str = obj[key];
|
||||||
|
if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
|
||||||
|
str = str.substring(prefix.length, str.length - suffix.length);
|
||||||
|
|
||||||
|
let binary_string = window.atob(str);
|
||||||
|
let len = binary_string.length;
|
||||||
|
let bytes = new Uint8Array(len);
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
bytes[i] = binary_string.charCodeAt(i);
|
||||||
|
}
|
||||||
|
obj[key] = bytes.buffer;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
recursiveBase64StrToArrayBuffer(obj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function passkey_check_browser()
|
||||||
|
{
|
||||||
|
// check browser support
|
||||||
|
if ((! window.fetch) || (! navigator.credentials) || (! navigator.credentials.create))
|
||||||
|
throw new Error('Browser not supported.');
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Availability of `window.PublicKeyCredential` means WebAuthn is usable.
|
||||||
|
// `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable.
|
||||||
|
// `isConditionalMediationAvailable` means the feature detection is usable.
|
||||||
|
if (window.PublicKeyCredential &&
|
||||||
|
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
|
||||||
|
PublicKeyCredential.isConditionalMediationAvailable) {
|
||||||
|
// Check if user verifying platform authenticator is available.
|
||||||
|
Promise.all([
|
||||||
|
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),
|
||||||
|
PublicKeyCredential.isConditionalMediationAvailable(),
|
||||||
|
]).then(results => {
|
||||||
|
if (results.every(r => r === true)) {
|
||||||
|
// Display "Create a new passkey" button
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (passkey_debug)
|
||||||
|
console.log('Passkey: Browser OK');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register/Create a passkey for a user
|
||||||
|
*/
|
||||||
|
async function passkey_register(csrf_token,icon_dom,icon,icon_shell_success,icon_shell_fail)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (! passkey_check_browser())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Change our icon so that it is obvious we are doing something
|
||||||
|
icon_dom.find('i').removeClass(icon).addClass('spinner-grow spinner-grow-sm');
|
||||||
|
|
||||||
|
// Get our arguments
|
||||||
|
var createArgs;
|
||||||
|
$.ajax({
|
||||||
|
url: '/passkey/register',
|
||||||
|
type: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
async: false,
|
||||||
|
cache: false,
|
||||||
|
success: function(data) {
|
||||||
|
if (passkey_debug)
|
||||||
|
console.log('Passkey: Get Register Success');
|
||||||
|
|
||||||
|
recursiveBase64StrToArrayBuffer(data);
|
||||||
|
createArgs = data;
|
||||||
|
},
|
||||||
|
error: function(e,status,error) {
|
||||||
|
throw new Error(status || 'Unknown error occurred');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create credentials
|
||||||
|
try {
|
||||||
|
const cred = await navigator.credentials.create(createArgs);
|
||||||
|
|
||||||
|
const authenticatorAttestationResponse = {
|
||||||
|
id: cred.id,
|
||||||
|
rawId: arrayBufferToBase64(cred.rawId),
|
||||||
|
transports: cred.response.getTransports ? cred.response.getTransports() : null,
|
||||||
|
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
|
||||||
|
attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null,
|
||||||
|
authenticatorAttachment: cred.authenticatorAttachment,
|
||||||
|
_token: csrf_token,
|
||||||
|
};
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/passkey/check',
|
||||||
|
type: 'POST',
|
||||||
|
data: authenticatorAttestationResponse,
|
||||||
|
cache: false,
|
||||||
|
success: function(data) {
|
||||||
|
if (passkey_debug)
|
||||||
|
console.log('Passkey: Registration Success');
|
||||||
|
|
||||||
|
icon_dom.find('i').addClass(icon).removeClass('spinner-grow spinner-grow-sm');
|
||||||
|
icon_dom.removeClass('btn-outline-primary').addClass('btn-primary');
|
||||||
|
},
|
||||||
|
error: function(e,status,error) {
|
||||||
|
throw new Error(status || 'Unknown error occurred');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (status) {
|
||||||
|
if (passkey_debug)
|
||||||
|
console.log(status || 'Passkey: User Aborted Register');
|
||||||
|
|
||||||
|
// Restore the icon
|
||||||
|
icon_dom.find('i').addClass(icon).removeClass('spinner-grow spinner-grow-sm');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
window.alert(err || 'An UNKNOWN error occurred?');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check a passkey being presented
|
||||||
|
*/
|
||||||
|
async function passkey_check(csrf_token,redirect)
|
||||||
|
{
|
||||||
|
if (passkey_debug)
|
||||||
|
console.log('Passkey: Check User Passkey');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (! passkey_check_browser())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Get our arguments
|
||||||
|
var getArgs;
|
||||||
|
$.ajax({
|
||||||
|
url: '/passkey/get',
|
||||||
|
type: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
async: false,
|
||||||
|
cache: false,
|
||||||
|
success: function(data) {
|
||||||
|
if (passkey_debug)
|
||||||
|
console.log('Passkey: Get Args Success');
|
||||||
|
|
||||||
|
recursiveBase64StrToArrayBuffer(data);
|
||||||
|
getArgs = data;
|
||||||
|
},
|
||||||
|
error: function(e,status,error) {
|
||||||
|
throw new Error(status || 'Unknown error occurred');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// check credentials with hardware
|
||||||
|
const cred = await navigator.credentials.get(getArgs);
|
||||||
|
|
||||||
|
// create object for transmission to server
|
||||||
|
const authenticatorAttestationResponse = {
|
||||||
|
id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
|
||||||
|
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
|
||||||
|
authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
|
||||||
|
signature: cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null,
|
||||||
|
userHandle: cred.response.userHandle ? arrayBufferToBase64(cred.response.userHandle) : null,
|
||||||
|
_token: csrf_token
|
||||||
|
};
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/passkey/process',
|
||||||
|
type: 'POST',
|
||||||
|
data: authenticatorAttestationResponse,
|
||||||
|
cache: false,
|
||||||
|
success: function(data) {
|
||||||
|
if (passkey_debug)
|
||||||
|
console.log('Passkey: Process Success');
|
||||||
|
|
||||||
|
// Direct to the home page
|
||||||
|
window.location.href = (redirect !== undefined) ? redirect : '/';
|
||||||
|
},
|
||||||
|
error: function(e,status,error) {
|
||||||
|
throw new Error(status || 'Unknown error occurred');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
window.alert(err || 'An UNKNOWN error occurred?');
|
||||||
|
}
|
||||||
|
}
|
13
src/routes.php
Normal file
13
src/routes.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Leenooks\Passkey\Controllers\PasskeyController;
|
||||||
|
|
||||||
|
Route::group(['middleware' => ['web']], function () {
|
||||||
|
Route::get('passkey/get',[PasskeyController::class,'args']);
|
||||||
|
Route::post('passkey/process',[PasskeyController::class,'process']);
|
||||||
|
|
||||||
|
Route::middleware(['auth','verified','activeuser'])->group(function () {
|
||||||
|
Route::post('passkey/check',[PasskeyController::class,'check']);
|
||||||
|
Route::get('passkey/register',[PasskeyController::class,'register']);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user