Webhooks
Webhook Handlers
Section titled “Webhook Handlers”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.
Webhook Flow
Section titled “Webhook Flow”- User scans QR code with ByteVault
- ByteVault signs the challenge with user’s private key
- ByteVault sends POST request to your webhook endpoint
- Your server verifies the signature
- Your server creates/authenticates the user
- Client polling detects the authenticated status
Default Webhook Controller
Section titled “Default Webhook Controller”The SDK includes a WebhookController that handles everything:
use ByteFederal\ByteAuthLaravel\Controllers\WebhookController;
// Registration webhookRoute::post('/webhook/registration', [WebhookController::class, 'handleRegistration']);
// Login webhookRoute::post('/webhook/login', [WebhookController::class, 'handleLogin']);
// Client pollingRoute::get('/api/check', [WebhookController::class, 'check']);Webhook Payload
Section titled “Webhook Payload”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" }}Payload Fields
Section titled “Payload Fields”| Field | Type | Description |
|---|---|---|
public_key | string | User’s hex-encoded public key |
signature | string | DER-encoded ECDSA signature |
challenge | string | The signed challenge string |
timestamp | integer | Unix timestamp of signature creation |
device_info | object | Information about the signing device |
Signature Verification
Section titled “Signature Verification”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);}Custom Webhook Handler
Section titled “Custom Webhook Handler”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']); }}Webhook Security
Section titled “Webhook Security”Signature Verification
Section titled “Signature Verification”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);}IP Whitelisting
Section titled “IP Whitelisting”For additional security, whitelist ByteAuth IP addresses:
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);}Error Responses
Section titled “Error Responses”Return appropriate HTTP status codes:
| Code | Meaning | When to Use |
|---|---|---|
200 | Success | Authentication successful |
404 | Not Found | User or challenge not found |
406 | Not Acceptable | Invalid signature |
408 | Timeout | Challenge expired |
409 | Conflict | User already exists (registration) |
500 | Server Error | Unexpected error |
Logging Webhooks
Section titled “Logging Webhooks”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}