Skip to content

Usage ​

How it works ​

The private key never leaves the device. Your API stores only the public key and proves the device holds the private key by asking it to sign a one-time challenge.

Registering a biometric (the user is already signed in)Mobile appYour APICreate a key pairprivate key stays on the deviceRegister the biometricbase64 public keyStore the public keycreateBiometric($publicKey)Biometric IDSave the private key and IDKeychain / Keystore
Registering a biometric (the user is already signed in)
Signing in with a biometricMobile appYour APIAsk for a challengebiometric IDIssue a challengegetBiometric($id)Challengevalid for 5 minutesUnlock with Face IDsign with the private keySend the signaturebiometric ID + base64 signatureVerify the signatureverifyBiometric($id, $signature)Signed inissue your token; challenge used up
Signing in with a biometric

Preparing the model ​

php
use Laranex\LaravelBiometricAuth\Traits\HasBiometrics;

class User extends Authenticatable
{
    use HasBiometrics;
}

Biometrics are stored with the model's morph class (getMorphClass()), so Relation::morphMap() aliases are respected.

Registering a biometric ​

Store the device's base64-encoded public key (PEM or DER). Invalid keys throw InvalidPublicKeyException.

php
$biometric = $user->createBiometric($publicKeyBase64);

$biometric->id; // UUID the device keeps

Every biometric of a model, revoked ones included, is available through the biometrics() morph-many relation. The active() scope keeps only biometrics that are not revoked:

php
$user->biometrics()->active()->get();

The public_key attribute is hidden when a Biometric is serialized.

Issuing a challenge ​

php
use Laranex\LaravelBiometricAuth\Facades\LaravelBiometricAuth;

$biometric = LaravelBiometricAuth::getBiometric($biometricId);

$biometric->challenge; // the device signs this

The challenge is a random 64-character hex string. It is kept until it is verified or expires, so calling getBiometric() twice before the device answers returns the same challenge. A challenge expires challenge.ttl seconds (5 minutes by default) after it was issued; the next getBiometric() call then issues a fresh one.

Biometric ids are UUIDs: any other id throws BiometricNotFoundException without querying the database.

Verifying the signature ​

The device signs the challenge with its private key and sends the signature in standard base64 (a signature that is not valid base64 counts as a failed attempt):

php
use Laranex\LaravelBiometricAuth\Models\Biometric;

$verified = LaravelBiometricAuth::verifyBiometric(
    $biometricId,
    $signatureBase64,
);

if ($verified) {
    // the model that registered the biometric
    $user = Biometric::find($biometricId)->instance;
}

Challenges are single-use: a successful verification clears the challenge, so a captured signature cannot be replayed. The challenge is consumed atomically, so when the same signature arrives twice at once only one request is verified. The next getBiometric() call issues a fresh one. An expired challenge is cleared and verifyBiometric() throws BiometricChallengeNotFoundException. A failed verification keeps the challenge so the device can retry, up to challenge.max_attempts failed attempts (5 by default, see Configuration); after that the challenge is cleared, verifyBiometric() throws BiometricChallengeNotFoundException and the client must call getBiometric() for a new one.

To resolve the service without the facade, use the container: app(\Laranex\LaravelBiometricAuth\LaravelBiometricAuth::class).

Revoking ​

Revoke one of the model's active biometrics so it can no longer be challenged or verified:

php
$user->revokeBiometric($biometricId);

Exceptions ​

All exceptions live in Laranex\LaravelBiometricAuth\Exceptions.

ExceptionHTTP statusThrown when
BiometricNotFoundException404getBiometric() / verifyBiometric() get an unknown, revoked or non-UUID biometric id, or revokeBiometric() gets a biometric that does not exist, is already revoked or belongs to another model
BiometricChallengeNotFoundException422verifyBiometric() is called with no pending challenge: none was issued, it was consumed by a successful verification, it was cleared after too many failed attempts, or it expired
InvalidPublicKeyException422createBiometric() or verifyBiometric() cannot load the public key

They extend Laranex\LaravelBiometricAuth\Exceptions\BiometricException. The status is also the exception code (getCode()) and is returned by getStatusCode(); passing a custom 4xx/5xx code to the constructor overrides it.

The exceptions are renderable: when the request expects JSON (an API call with Accept: application/json), an uncaught exception becomes a JSON error with that status:

json
{ "message": "Biometric not found" }

Other requests fall through to your application's exception handler. Catch the exceptions, or register your own renderer with $exceptions->render() (Laravel 11+) or $this->renderable() in the exception handler (Laravel 10), to change the response.

Released under the MIT License, except where a package says otherwise.