Skip to content

Webhooks

When a user authenticates with ByteVault, the app sends a webhook to your server containing the signed challenge and user’s public key. Your server must verify the signature and complete the authentication.

  1. User scans QR code with ByteVault
  2. ByteVault signs the challenge with user’s private key
  3. ByteVault sends POST request to your webhook endpoint
  4. Your server verifies the signature
  5. Your server creates/authenticates the user
  6. Client polling detects the authenticated status

The SDK includes a WebhookController that handles everything:

use ByteFederal\ByteAuthLaravel\Controllers\WebhookController;
// Registration webhook
Route::post('/webhook/registration', [WebhookController::class, 'handleRegistration']);
// Login webhook
Route::post('/webhook/login', [WebhookController::class, 'handleLogin']);
// Client polling
Route::get('/api/check', [WebhookController::class, 'check']);

ByteVault sends the following JSON payload:

{
"public_key": "04a1b2c3d4e5f6...",
"signature": "3045022100...",
"challenge": "Sign this to login to example.com at 1699876543:abc123...",
"timestamp": 1699876545,
"device_info": {
"platform": "ios",
"version": "2.1.0"
}
}
FieldTypeDescription
public_keystringUser’s hex-encoded public key
signaturestringDER-encoded ECDSA signature
challengestringThe signed challenge string
timestampintegerUnix timestamp of signature creation
device_infoobjectInformation about the signing device

The SDK automatically verifies signatures, but here’s what happens:

use ByteFederal\ByteAuthLaravel\Services\SignatureVerifier;
$verifier = new SignatureVerifier();
$isValid = $verifier->verify(
publicKey: $request->public_key,
signature: $request->signature,
message: $request->challenge
);
if (!$isValid) {
return response()->json(['error' => 'Invalid signature'], 406);
}

For full control, create your own controller:

<?php
namespace App\Http\Controllers;
use App\Models\User;
use ByteFederal\ByteAuthLaravel\Services\SignatureVerifier;
use ByteFederal\ByteAuthLaravel\Services\ChallengeManager;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ByteAuthController extends Controller
{
public function __construct(
private SignatureVerifier $verifier,
private ChallengeManager $challenges
) {}
public function handleLogin(Request $request)
{
// 1. Validate request
$validated = $request->validate([
'public_key' => 'required|string',
'signature' => 'required|string',
'challenge' => 'required|string',
'timestamp' => 'required|integer',
]);
// 2. Verify timestamp is recent (within 30 seconds)
if (abs(time() - $validated['timestamp']) > 30) {
return response()->json([
'error' => 'Challenge expired'
], 408);
}
// 3. Verify the challenge exists
$session = $this->challenges->findByChallenge($validated['challenge']);
if (!$session) {
return response()->json([
'error' => 'Challenge not found'
], 404);
}
// 4. Verify signature
if (!$this->verifier->verify(
$validated['public_key'],
$validated['signature'],
$validated['challenge']
)) {
return response()->json([
'error' => 'Invalid signature'
], 406);
}
// 5. Find or reject user
$user = User::where('public_key', $validated['public_key'])->first();
if (!$user) {
return response()->json([
'error' => 'User not registered'
], 404);
}
// 6. Mark session as authenticated
$this->challenges->authenticate($session, $user);
// 7. Custom logic
$user->update(['last_login_at' => now()]);
return response()->json([
'status' => 'authenticated',
'message' => 'Login successful'
]);
}
public function handleRegistration(Request $request)
{
// Similar to login, but creates user if not exists
$validated = $request->validate([
'public_key' => 'required|string',
'signature' => 'required|string',
'challenge' => 'required|string',
'timestamp' => 'required|integer',
]);
// Verify timestamp
if (abs(time() - $validated['timestamp']) > 30) {
return response()->json(['error' => 'Challenge expired'], 408);
}
// Verify challenge
$session = $this->challenges->findByChallenge($validated['challenge']);
if (!$session) {
return response()->json(['error' => 'Challenge not found'], 404);
}
// Verify signature
if (!$this->verifier->verify(
$validated['public_key'],
$validated['signature'],
$validated['challenge']
)) {
return response()->json(['error' => 'Invalid signature'], 406);
}
// Check if user already exists
$existingUser = User::where('public_key', $validated['public_key'])->first();
if ($existingUser) {
return response()->json([
'error' => 'User already registered'
], 409);
}
// Create new user
$user = User::create([
'public_key' => $validated['public_key'],
'name' => 'ByteAuth User',
'email' => substr($validated['public_key'], 0, 16) . '@byteauth.local',
'password' => bcrypt(Str::random(32)),
]);
// Mark session as authenticated
$this->challenges->authenticate($session, $user);
// Send welcome notification
$user->notify(new WelcomeNotification());
return response()->json([
'status' => 'registered',
'message' => 'Registration successful'
]);
}
public function check(Request $request)
{
$sessionId = $request->query('sid');
if (!$sessionId) {
return response()->json(['error' => 'Session ID required'], 400);
}
$session = $this->challenges->find($sessionId);
if (!$session) {
return response()->json(['status' => 'not_found'], 404);
}
if ($session->authenticated_at) {
// Log the user in
Auth::login($session->user);
return response()->json([
'status' => 'authenticated',
'redirect' => '/dashboard'
]);
}
return response()->json(['status' => 'pending']);
}
}

Always verify webhook signatures to ensure requests come from ByteVault:

public function verifyWebhookSignature(Request $request): bool
{
$signature = $request->header('X-ByteAuth-Signature');
$payload = $request->getContent();
$secret = config('byteauth.webhook_secret');
$expected = hash_hmac('sha256', $payload, $secret);
return hash_equals($expected, $signature);
}

For additional security, whitelist ByteAuth IP addresses:

app/Http/Middleware/VerifyByteAuthIP.php
public function handle($request, Closure $next)
{
$allowedIPs = [
'52.xxx.xxx.xxx',
'54.xxx.xxx.xxx',
];
if (!in_array($request->ip(), $allowedIPs)) {
abort(403, 'Forbidden');
}
return $next($request);
}

Return appropriate HTTP status codes:

CodeMeaningWhen to Use
200SuccessAuthentication successful
404Not FoundUser or challenge not found
406Not AcceptableInvalid signature
408TimeoutChallenge expired
409ConflictUser already exists (registration)
500Server ErrorUnexpected error

Log webhook activity for debugging:

use Illuminate\Support\Facades\Log;
public function handleLogin(Request $request)
{
Log::channel('byteauth')->info('Login webhook received', [
'public_key' => substr($request->public_key, 0, 16) . '...',
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
// ... handle webhook
}