Want to make your Laravel Backpack admin panel more secure with a unique login experience for your admins? I'll show you how to add Pas...
Want to make your Laravel Backpack admin panel more secure with a unique login experience for your admins?
I'll show you how to add Passkeys - it's like using your phone's face scanning/fingerprint to log in. No more remembering passwords! It uses FIDO2, the same technology that big companies like Google and Apple trust.
Follow along with this step-by-step guide, and you'll have Passkeys working in your admin panel - no deep technical knowledge required! 👇
composer require web-auth/webauthn-lib:^5.0
Create the file: app/Support/JsonSerializer.php
. This helper class serializes and deserializes the WebAuthn credential data for passkey authentication.
<?php
/**
* WebAuthn JSON Serializer
*
* Originally from Laracasts "Adding Passkeys to Your Laravel App" course
*
* @author Luke Raymond Downing <github.com/lukeraymonddowning>
*
* @source https://github.com/laracasts/adding-passkeys-to-your-laravel-app/blob/v5/app/Support/JsonSerializer.php
*/
namespace App\Support;
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
use Webauthn\Denormalizer\WebauthnSerializerFactory;
class JsonSerializer
{
public static function serialize(object $data): string
{
return (new WebauthnSerializerFactory(AttestationStatementSupportManager::create()))
->create()
->serialize($data, 'json');
}
/**
* @template TReturn
*
* @param class-string<TReturn> $into
* @return TReturn
*/
public static function deserialize(string $json, string $into)
{
return (new WebauthnSerializerFactory(AttestationStatementSupportManager::create()))
->create()
->deserialize($json, $into, 'json');
}
}
php artisan make:model Passkey --migration
Here is the essential migration schema for the passkeys
table.
public function up(): void
{
Schema::create('passkeys', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->string('name');
$table->binary('credential_id');
$table->json('data');
$table->timestamps();
});
}
Reminder: Run
php artisan migrate
to create passkeys table
The corresponding Passkey
model uses the helper class for the accessor & mutator to correctly parse the credential data.
<?php
namespace App\Models;
use App\Support\JsonSerializer;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Webauthn\PublicKeyCredentialSource;
class Passkey extends Model
{
protected $fillable = [
'name',
'credential_id',
'data',
];
/*
|--------------------------------------------------------------------------
| RELATIONS
|--------------------------------------------------------------------------
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/*
|--------------------------------------------------------------------------
| ACCESSORS / MUTATORS
|--------------------------------------------------------------------------
*/
public function data(): Attribute
{
return new Attribute(
get: fn (string $value) => JsonSerializer::deserialize($value, PublicKeyCredentialSource::class),
set: fn (PublicKeyCredentialSource $value) => [
'credential_id' => $value->publicKeyCredentialId,
'data' => JsonSerializer::serialize($value),
],
);
}
}
Don’t forget to add the relationship in the User
class.
class User extends Authenticatable
{
// Existing code ...
public function passkeys(): HasMany
{
return $this->hasMany(Passkey::class);
}
}
Extend the \Backpack\CRUD\app\Http\Controllers\MyAccountController
, as we are going to add a Passkey management section below the update password section.
In your app/Providers/AppServiceProvider.php
public function boot(): void
{
// some other existing code ...
// Customize Controllers
$this->app->bind(
\Backpack\CRUD\app\Http\Controllers\MyAccountController::class,
\App\Http\Controllers\Admin\MyAccountController::class
);
}
I am injecting the user’s $passkeys
and $passkey_register_options
(which contains the Attestation options) to the view in the extended MyAccountController
. These attestation options are minimum to work. You can check the documentation to learn more.
app/Http/Controllers/Admin/MyAccountController.php
<?php
namespace App\Http\Controllers\Admin;
use App\Support\JsonSerializer;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use Webauthn\Exception\InvalidDataException;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRpEntity;
use Webauthn\PublicKeyCredentialUserEntity;
class MyAccountController extends \Backpack\CRUD\app\Http\Controllers\MyAccountController
{
/**
* @throws InvalidDataException
*/
public function getAccountInfoForm()
{
$this->data['title'] = trans('backpack::base.my_account');
$this->data['user'] = $this->guard()->user();
$this->data['passkeys'] = $this->guard()->user()->passkeys()->select(['id', 'name', 'created_at'])->get();
Session::flash('passkey_register_options', $this->getRegisterOptions());
return view(backpack_view('my_account'), $this->data);
}
/**
* Generate WebAuthn registration options for credential creation.
* Necessary data including relying party details, user information, and a random challenge.
*
* @throws InvalidDataException
*/
private function getRegisterOptions()
{
return new PublicKeyCredentialCreationOptions(
rp: new PublicKeyCredentialRpEntity(
name: config('app.name'),
id: parse_url(config('app.url'), PHP_URL_HOST),
),
user: new PublicKeyCredentialUserEntity(
name: $this->guard()->user()->email,
id: $this->guard()->user()->id,
displayName: $this->guard()->user()->name,
),
challenge: Str::random(),
);
}
}
Now, we need to work on blade views. Copy vendor/backpack/theme-tabler/resources/views/my_account.blade.php
to resources/views/vendor/backpack/theme-tabler/my_account.blade.php
to override the original view, and add the passkey section after CHANGE PASSWORD FORM
.
{{-- Passkeys Section --}}
@include('vendor.backpack.theme-tabler.passkeys')
Create your own passkeys.blade.php
file, or use mine as your starting point.
<div class="col-lg-8 mb-4">
<div class="card">
<div class="card-header">
<h3 class="card-title">Manage Passkeys</h3>
</div>
<div class="card-body backpack-profile-form bold-labels">
{{-- REGISTERED PASSKEYS LIST --}}
@if(count($passkeys ?? []) > 0)
<div class="table-responsive">
<table class="table" style="width:100%">
<thead>
<tr>
<th>Name</th>
<th>Created At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach($passkeys as $passkey)
<tr>
<td>{{ $passkey->name }}</td>
<td>{{ $passkey->created_at->diffForHumans() }}</td>
<td>
<button type="button" class="btn btn-sm btn-danger">
<i class="la la-trash"></i> Delete
</button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<div class="alert alert-info">
No passkeys registered yet.
</div>
@endif
{{-- REGISTER NEW PASSKEY --}}
<form id="passkey-form" class="form mt-4" action="{{ route('backpack.passkey.create') }}" method="POST">
@csrf
<div class="row">
<div class="col-md-6 form-group">
<label class="required">Name</label>
<input required class="form-control" type="text" name="name" value="{{ old('name') }}"
placeholder="Enter a name for this passkey">
</div>
</div>
<input type="hidden" id="passkey" name="passkey" value="">
<div class="mt-3">
<button type="submit" class="btn btn-success">
<i class="la la-key"></i> Register New Passkey
</button>
</div>
</form>
</div>
</div>
</div>
First, add the routes to routes/backpack/custom.php
.
use App\Http\Controllers\Admin\PasskeyController;
Route::group([
'prefix' => config('backpack.base.route_prefix', 'admin'),
'middleware' => array_merge(
(array) config('backpack.base.web_middleware', 'web'),
(array) config('backpack.base.middleware_key', 'admin')
),
'namespace' => 'App\Http\Controllers\Admin',
], function () { // custom admin routes
Route::post('passkey/create', [PasskeyController::class, 'create'])->name('backpack.passkey.create');
// other routes ...
});
Then, create app/Http/Controllers/Admin/PasskeyController.php
. It will handle the Passkey creation and deletion.
The create
method accepts the name that user provided and the Passkey that is returned by the javascript package @simplewebauthn/browser with the registration option provided in the session
.
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Support\JsonSerializer;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Validation\ValidationException;
use Prologue\Alerts\Facades\Alert;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\AuthenticatorAttestationResponseValidator;
use Webauthn\CeremonyStep\CeremonyStepManagerFactory;
use Webauthn\PublicKeyCredential;
use Webauthn\PublicKeyCredentialCreationOptions;
class PasskeyController extends Controller
{
public function create(Request $request): RedirectResponse
{
$user = $this->guard()->user();
$validated = $request->validate([
'name' => ['required', 'string', 'between:1,255'],
'passkey' => ['required', 'json'],
]);
// Deserialize the public key credential from the request
$publicKeyCredential = JsonSerializer::deserialize($validated['passkey'], PublicKeyCredential::class);
if (! $publicKeyCredential->response instanceof AuthenticatorAttestationResponse) {
return redirect()->guest(backpack_url('login'));
}
try {
$publicKeyCredentialSource = AuthenticatorAttestationResponseValidator::create(
(new CeremonyStepManagerFactory)->creationCeremony(),
)->check(
authenticatorAttestationResponse: $publicKeyCredential->response,
publicKeyCredentialCreationOptions: Session::get('passkey_register_options'),
host: $request->getHost(),
);
} catch (\Throwable $e) {
throw ValidationException::withMessages([
'name' => 'The given passkey is invalid.',
])->errorBag('createPasskey');
}
$result = $user->passkeys()->create([
'name' => $validated['name'],
'data' => $publicKeyCredentialSource,
]);
if ($result) {
Alert::success('Passkey created successfully')->flash();
} else {
Alert::error(trans('Failed to create Passkey'))->flash();
}
return redirect()->back();
}
protected function guard()
{
return backpack_auth();
}
}
Finally, add the script at the top of passkey.blade.php
created in the previous step.
@section('after_scripts')
@basset('https://unpkg.com/@simplewebauthn/[email protected]/dist/bundle/index.umd.min.js')
<script>
const form = document.getElementById('passkey-form');
const registrationOptions = {!! json_encode(\App\Support\JsonSerializer::serialize(session('passkey_register_options'))) !!};
form.addEventListener('submit', async function (e) {
e.preventDefault();
// Name validation with length check
const name = document.querySelector('input[name="name"]').value.trim();
if (!name || name.length < 1 || name.length > 255) {
alert('Name must be between 1 and 255 characters');
return;
}
try {
const options = JSON.parse(registrationOptions);
const attResp = await SimpleWebAuthnBrowser.startRegistration(options);
// Add the attestation to the form
document.getElementById('passkey').value = JSON.stringify(attResp);
this.submit();
} catch (error) {
console.error('Error:', error);
alert('Failed to register passkey: ' + error.message);
}
});
</script>
@endsection
Now, you should be able to create your first passkey! 🎉
Let’s upgrade the login form to accept the user’s email and provide the challenge for the passkey authentication process.
Extend the \Backpack\CRUD\app\Http\Controllers\Auth\LoginController
, as we are going to provide the assertion options for the WebAuthn authentication challenge.
Registration involves Attestation where a new security key is registered, while authentication uses Assertion where an existing security key proves its identity.
In your app/Providers/AppServiceProvider.php
, add these lines...
public function boot(): void
{
// some other existing code ...
$this->app->bind(
\Backpack\CRUD\app\Http\Controllers\Auth\LoginController::class,
\App\Http\Controllers\Admin\Auth\LoginController::class
);
}
Then create app/Http/Controllers/Admin/Auth/LoginController.php
and override the showLoginForm()
function.
<?php
namespace App\Http\Controllers\Admin\Auth;
use App\Models\Passkey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
class LoginController extends \Backpack\CRUD\app\Http\Controllers\Auth\LoginController
{
/**
* Extend the application's login form.
*
* @return \Illuminate\Contracts\View\View
*/
public function showLoginForm()
{
$this->data['title'] = trans('backpack::base.login'); // set the page title
$this->data['username'] = $this->username();
$this->data['valid_passkey_challenge'] = Session::has('passkey_authentication_options');
// Only keep passkey authentication options if username exists
if (old($this->username())) {
Session::keep('passkey_authentication_options');
}
return view(backpack_view('auth.login'), $this->data);
}
/**
* Generate WebAuthn authentication options for passkey-based login.
*/
public function authenticateOptions(Request $request)
{
$validated = $request->validate([
'email' => ['required', 'email', 'max:255'],
]);
$allowedCredentials = $request->query('email')
? Passkey::whereRelation('user', 'email', $validated['email'])
->get()
->map(fn (Passkey $passkey) => $passkey->data)
->map(fn (PublicKeyCredentialSource $publicKeyCredentialSource) => $publicKeyCredentialSource->getPublicKeyCredentialDescriptor())
->all()
: [];
$options = new PublicKeyCredentialRequestOptions(
challenge: Str::random(),
rpId: parse_url(config('app.url'), PHP_URL_HOST),
allowCredentials: $allowedCredentials,
);
Session::flash('passkey_authentication_options', $options);
return redirect()->back()
->withInput(['email' => $validated['email']]);
}
}
Add these lines to routes/backpack/custom.php
to expose the authenticateOptions()
function.
use App\Http\Controllers\Admin\Auth\LoginController;
// --------------------------
// Passkey Sign-In Routes
// --------------------------
Route::group([
'prefix' => config('backpack.base.route_prefix', 'admin'),
'middleware' => array_merge(
(array) config('backpack.base.web_middleware', 'web'),
['throttle:5,1'] // 5 attempts per minute
),
'namespace' => 'App\Http\Controllers\Admin\Auth',
], function () {
Route::post('passkey/login', [LoginController::class, 'authenticateOptions'])->name('passkey.login');
});
Finally, create the blade view to render the necessary script to handle WebAuthn assertion.
Add the script snippet below to the @section('after_scripts')
section in the copied resources/views/vendor/backpack/theme-tabler/auth/login/inc/form.blade.php
@basset('https://unpkg.com/@simplewebauthn/[email protected]/dist/bundle/index.umd.min.js')
<script>
// Setup passkey authentication
const $emailInput = $(`input[name="{{ $username }}"]`);
const $passkeyButton = $('#btn-passkey-auth');
@if(!$valid_passkey_challenge)
// Email validation for initial passkey button
$emailInput.on('input', () => {
const isValid = $emailInput.val().includes('@') && $emailInput.val().includes('.');
$passkeyButton.prop('disabled', !isValid).toggleClass('d-none', !isValid);
});
// Initial check for pre-filled email
$emailInput.trigger('input');
// Handle passkey button click with form submision
$passkeyButton.on('click', () => {
$passkeyButton.prop('disabled', true);
$('body').css('cursor', 'wait');
$('<form>', {
method: 'POST',
action: '{{ route('passkey.login') }}',
html: `
<input type="hidden" name="_token" value="${$('meta[name="csrf-token"]').attr('content')}">
<input type="hidden" name="email" value="${$emailInput.val()}">
`
}).appendTo('body').submit();
});
@else
// Handle the actual authentication
$passkeyButton.on('click', async () => {
// TODO: Passkey Authentication
});
@endif
</script>
The <form>
in the blade needs to be updated for better UX. When a valid email is provided, a button "Sign in with passkey" will appear. Once the assertion challenge is received from the server, the password-related elements will disappear. While not the optimal UX, it should suffice for demonstration purposes.
<form method="POST" action="{{ route('backpack.auth.login') }}" autocomplete="off" novalidate="">
@csrf
<div class="mb-3">
<label class="form-label" for="{{ $username }}">{{ trans('backpack::base.'.strtolower(config('backpack.base.authentication_column_name'))) }}</label>
<input autofocus tabindex="1" type="text" name="{{ $username }}" value="{{ old($username) }}" id="{{ $username }}" class="form-control {{ $errors->has($username) ? 'is-invalid' : '' }}" {{ $valid_passkey_challenge ? 'disabled' : '' }}>
@if ($errors->has($username))
<div class="invalid-feedback">{{ $errors->first($username) }}</div>
@endif
</div>
<div class="mb-2 {{ $valid_passkey_challenge ? 'd-none' : '' }}">
<label class="form-label" for="password">
{{ trans('backpack::base.password') }}
</label>
<div class="input-group input-group-flat password-visibility-toggler">
<input tabindex="2" type="password" name="password" id="password" class="form-control {{ $errors->has('password') ? 'is-invalid' : '' }}" value="">
@if(backpack_theme_config('options.showPasswordVisibilityToggler'))
<span class="input-group-text p-0 px-2">
<a href="#" data-input-type="text" class="link-secondary p-2" data-bs-toggle="tooltip" aria-label="{{ trans('backpack.theme-tabler::theme-tabler.password-show') }}" data-bs-original-title="{{ trans('backpack.theme-tabler::theme-tabler.password-show') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-eye" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6"></path></svg>
</a>
<a href="#" data-input-type="password" class="link-secondary p-2 d-none" data-bs-toggle="tooltip" aria-label="{{ trans('backpack.theme-tabler::theme-tabler.password-hide') }}" data-bs-original-title="{{ trans('backpack.theme-tabler::theme-tabler.password-hide') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-eye-off" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" /><path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" /><path d="M3 3l18 18" /></svg>
</a>
</span>
@endif
</div>
@if ($errors->has('password'))
<div class="invalid-feedback">{{ $errors->first('password') }}</div>
@endif
</div>
<div class="d-flex justify-content-between align-items-center mb-2 {{ $valid_passkey_challenge ? 'd-none' : '' }}">
<label class="form-check mb-0">
<input name="remember" tabindex="3" type="checkbox" class="form-check-input">
<span class="form-check-label">{{ trans('backpack::base.remember_me') }}</span>
</label>
@if (backpack_users_have_email() && backpack_email_column() == 'email' && config('backpack.base.setup_password_recovery_routes', true))
<div class="form-label-description">
<a tabindex="4" href="{{ route('backpack.auth.password.reset') }}">{{ trans('backpack::base.forgot_your_password') }}</a>
</div>
@endif
</div>
<div class="form-footer">
<button tabindex="5" id="btn-passkey-auth" type="button"
class="btn w-100 mb-2 {{ $valid_passkey_challenge ? 'btn-primary' : 'd-none btn-success' }}">
{{ $valid_passkey_challenge ? 'Login with passkey' : 'I\'ve passkey registered!' }}
</button>
<button tabindex="5" type="submit" class="btn btn-primary w-100 {{ $valid_passkey_challenge ? 'd-none' : '' }}">{{ trans('backpack::base.login') }}</button>
</div>
</form>
Simple Passkey Frontend Demo
If everything works correctly, congratulations! Also, thank you for following the tutorial up to this point. You’ve reached the last part!
Add the authentication endpoint in the route file routes/backpack/custom.php
Route::post('passkeys/authenticate', [LoginController::class, 'authenticatePasskey'])->name('passkey.authenticate');
with the corresponding controller method in LoginController
:
use App\Support\JsonSerializer;
use Illuminate\Validation\ValidationException;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAssertionResponseValidator;
use Webauthn\CeremonyStep\CeremonyStepManagerFactory;
use Webauthn\PublicKeyCredential;
/**
* Authenticate a user using a WebAuthn passkey.
*/
public function authenticatePasskey(Request $request)
{
$validated = $request->validate([
'answer' => ['required', 'json'],
]);
// Deserialize the answer from the request
$publicKeyCredential = JsonSerializer::deserialize($validated['answer'], PublicKeyCredential::class);
if (! $publicKeyCredential->response instanceof AuthenticatorAssertionResponse) {
return redirect()->guest(backpack_url('login'));
}
$passkey = Passkey::firstWhere('credential_id', $publicKeyCredential->rawId);
if (! $passkey) {
throw ValidationException::withMessages(['email' => 'The passkey is invalid.']);
}
try {
$publicKeyCredentialSource = AuthenticatorAssertionResponseValidator::create(
(new CeremonyStepManagerFactory)->requestCeremony()
)->check(
publicKeyCredentialSource: $passkey->data,
authenticatorAssertionResponse: $publicKeyCredential->response,
publicKeyCredentialRequestOptions: Session::get('passkey_authentication_options'),
host: $request->getHost(),
userHandle: null,
);
} catch (\Throwable $e) {
throw ValidationException::withMessages([
'email' => 'The passkey is invalid.',
]);
}
$passkey->update(['data' => $publicKeyCredentialSource]);
// Login the user
$this->guard()->loginUsingId($passkey->user_id);
$request->session()->regenerate();
return redirect()->intended($this->redirectPath());
}
and the final puzzle, the frontend method to send the assertion response.
Locate the section // Handle the actual authentication
in the file resources/views/vendor/backpack/theme-tabler/auth/login/inc/form.blade.php
$passkeyButton.on('click', async () => {
try {
$passkeyButton.prop('disabled', true);
$('body').css('cursor', 'wait');
const authenticationOptions = {!! json_encode(\App\Support\JsonSerializer::serialize(session('passkey_authentication_options'))) !!};
if (!authenticationOptions) {
throw new Error('Authentication options not found');
}
// Start the authentication process
const options = JSON.parse(authenticationOptions);
const credential = await SimpleWebAuthnBrowser.startAuthentication(options);
// Create and submit form with credential
$('<form>', {
method: 'POST',
action: '{{ route('passkey.authenticate') }}',
html: `
<input type="hidden" name="_token" value="${$('meta[name="csrf-token"]').attr('content')}">
<input type="hidden" name="answer" value='${JSON.stringify(credential)}'>
`
}).appendTo('body').submit();
} catch (error) {
console.error('Passkey authentication error:', error);
$passkeyButton.prop('disabled', false);
$('body').css('cursor', 'default');
new Noty({
type: 'error',
text: 'Passkey authentication failed. Please try again.',
timeout: 5000
}).show();
window.location.href = '{{ backpack_url() }}';
}
});
Let’s complete the passkey management functionality by making the delete button work. This is just a simple sweetalert code copied from delete operations in Backpack.
Add this function in your @section('after_scripts')
in passkey.blade.php
.
// Delete passkey function
function deleteEntry(button) {
var route = $(button).attr('data-route');
swal({
title: "{!! trans('backpack::base.warning') !!}",
text: "{!! trans('backpack::crud.delete_confirm') !!}",
icon: "warning",
buttons: {
cancel: {
text: "{!! trans('backpack::crud.cancel') !!}",
value: null,
visible: true,
className: "bg-secondary",
closeModal: true,
},
delete: {
text: "{!! trans('backpack::crud.delete') !!}",
value: true,
visible: true,
className: "bg-danger",
}
},
dangerMode: true,
}).then((value) => {
if (value) {
$.ajax({
url: route,
type: 'DELETE',
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
success: function (result) {
if (result == 1) {
// Remove the row from the table
$(button).closest('tr').fadeOut(function () {
$(this).remove();
// If no more rows, show the "no passkeys" message
if ($('table tbody tr').length === 0) {
$('.table-responsive').replaceWith(
'<div class="alert alert-info">You have no passkeys registered yet.</div>'
);
}
});
// Show a success notification
new Noty({
type: "success",
text: "{!! '<strong>'.trans('backpack::crud.delete_confirmation_title').'</strong><br>'.trans('backpack::crud.delete_confirmation_message') !!}"
}).show();
} else {
// Show an error alert
swal({
title: "{!! trans('backpack::crud.delete_confirmation_not_title') !!}",
text: "{!! trans('backpack::crud.delete_confirmation_not_message') !!}",
icon: "error",
timer: 4000,
buttons: false,
});
}
},
error: function (result) {
// Show an error alert
swal({
title: "{!! trans('backpack::crud.delete_confirmation_not_title') !!}",
text: "{!! trans('backpack::crud.delete_confirmation_not_message') !!}",
icon: "error",
timer: 4000,
buttons: false,
});
}
});
}
});
}
Don’t forget to attach it to the button.
<button type="button" class="btn btn-sm btn-danger"
onclick="deleteEntry(this)"
data-route="{{ route('backpack.passkey.delete', $passkey->id) }}">
<i class="la la-trash"></i> Delete
</button>
Finally, create the corresponding route in routes/backpack/custom.php
and the method in the backend controller.
Route::delete('passkey/{id}', [PasskeyController::class, 'destroy'])->name('backpack.passkey.delete');
public function destroy($id): string
{
// For new passkey creation
Session::keep('passkey_register_options');
$user = $this->guard()->user();
// Find the passkey and ensure it belongs to the current user
$passkey = $user->passkeys()->find($id);
if (! $passkey) {
return '0'; // Return 0 when passkey missing
}
try {
if ($passkey->delete()) {
return '1'; // Return 1 for success
}
return '0'; // Return 0 for failure
} catch (\Exception $e) {
return '0';
}
}
By now, you have added passkey authentication to your Laravel Backpack admin panel. I hope you enjoyed this tutorial. You can check the working source code on Github: kiddtang/backpack-passkey-auth.
Special thanks to Luke Downing and his excellent Laracasts course “Adding Passkeys to Your Laravel App” which served as a foundational inspiration for this tutorial.
Subscribe to our "Article Digest". We'll send you a list of the new articles, every week, month or quarter - your choice.
What do you think about this?
Wondering what our community has been up to?