Laravel SDK Guide
Installation
bash
composer require guzzlehttp/guzzleService Provider
php
<?php
// app/Providers/KeyClaimServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Services\KeyClaimService;
class KeyClaimServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(KeyClaimService::class, function ($app) {
return new KeyClaimService(
config('services.keyclaim.api_key'),
config('services.keyclaim.base_url', 'https://keyclaim.org/api')
);
});
}
}Service Implementation
php
<?php
// app/Services/KeyClaimService.php
namespace App\Services;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
class KeyClaimService
{
private $client;
private $apiKey;
private $baseUrl;
public function __construct($apiKey, $baseUrl = 'https://keyclaim.org/api')
{
$this->client = new Client();
$this->apiKey = $apiKey;
$this->baseUrl = $baseUrl;
}
/**
* Create a new challenge
*/
public function createChallenge($ttl = 30)
{
try {
$response = $this->client->post("$this->baseUrl/challenge/create", [
'headers' => [
'Authorization' => 'Bearer ' . $this->apiKey,
'Content-Type' => 'application/json',
],
'json' => [
'ttl' => $ttl
]
]);
return json_decode($response->getBody(), true);
} catch (RequestException $e) {
throw new \Exception('Failed to create challenge: ' . $e->getMessage());
}
}
/**
* Generate HMAC-SHA256 response
*/
public function generateResponseHMAC($challenge)
{
return hash_hmac('sha256', $challenge, $this->apiKey);
}
/**
* Decrypt challenge with private key (RSA)
*/
public function decryptChallenge($encryptedChallenge, $privateKeyPem)
{
$encrypted = base64_decode($encryptedChallenge);
$decrypted = '';
if (!openssl_private_decrypt(
$encrypted,
$decrypted,
$privateKeyPem,
OPENSSL_PKCS1_OAEP_PADDING
)) {
throw new \Exception('Failed to decrypt challenge');
}
return $decrypted;
}
/**
* Validate challenge-response pair
*/
public function validate($challenge, $response, $decryptedChallenge = null)
{
try {
$payload = [
'key' => $this->apiKey,
'challenge' => $challenge,
'response' => $response
];
if ($decryptedChallenge !== null) {
$payload['decryptedChallenge'] = $decryptedChallenge;
}
$response = $this->client->post("$this->baseUrl/challenge/validate", [
'json' => $payload
]);
return json_decode($response->getBody(), true);
} catch (RequestException $e) {
throw new \Exception('Failed to validate: ' . $e->getMessage());
}
}
/**
* Convenience method to validate a challenge
*/
public function validateChallenge($challenge, $method = 'hmac', $privateKeyPem = null)
{
if ($method === 'hmac') {
$response = $this->generateResponseHMAC($challenge);
return $this->validate($challenge, $response);
} elseif ($method === 'rsa' && $privateKeyPem) {
$decrypted = $this->decryptChallenge($challenge, $privateKeyPem);
return $this->validate($challenge, $decrypted, $decrypted);
} else {
throw new \Exception("Unknown method: $method");
}
}
}Configuration
php
// config/services.php
return [
'keyclaim' => [
'api_key' => env('KEYCLAIM_API_KEY'),
'base_url' => env('KEYCLAIM_BASE_URL', 'https://keyclaim.org/api'),
],
];Usage in Controllers
php
<?php
// app/Http/Controllers/ChallengeController.php
namespace App\Http\Controllers;
use App\Services\KeyClaimService;
use Illuminate\Http\Request;
class ChallengeController extends Controller
{
protected $keyClaim;
public function __construct(KeyClaimService $keyClaim)
{
$this->keyClaim = $keyClaim;
}
public function create()
{
try {
$result = $this->keyClaim->createChallenge();
return response()->json($result);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
public function validate(Request $request)
{
$request->validate([
'challenge' => 'required|string',
]);
try {
$challenge = $request->input('challenge');
$result = $this->keyClaim->validateChallenge($challenge, 'hmac');
return response()->json($result);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
}Middleware Example
php
<?php
// app/Http/Middleware/ValidateKeyClaim.php
namespace App\Http\Middleware;
use Closure;
use App\Services\KeyClaimService;
use Illuminate\Http\Request;
class ValidateKeyClaim
{
protected $keyClaim;
public function __construct(KeyClaimService $keyClaim)
{
$this->keyClaim = $keyClaim;
}
public function handle(Request $request, Closure $next)
{
$challenge = $request->header('X-KeyClaim-Challenge');
$response = $request->header('X-KeyClaim-Response');
if (!$challenge || !$response) {
return response()->json(['error' => 'Missing challenge or response'], 401);
}
try {
$result = $this->keyClaim->validate($challenge, $response);
if (!$result['valid']) {
return response()->json(['error' => 'Invalid challenge response'], 401);
}
// Store validation result in request for later use
$request->merge(['keyclaim_validated' => true]);
return $next($request);
} catch (\Exception $e) {
return response()->json(['error' => 'Validation failed'], 500);
}
}
}Artisan Command
php
<?php
// app/Console/Commands/TestKeyClaim.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\KeyClaimService;
class TestKeyClaim extends Command
{
protected $signature = 'keyclaim:test';
protected $description = 'Test KeyClaim integration';
public function handle(KeyClaimService $keyClaim)
{
$this->info('Creating challenge...');
$result = $keyClaim->createChallenge();
$challenge = $result['challenge'];
$this->info("Challenge: $challenge");
$this->info('Generating response...');
$response = $keyClaim->generateResponseHMAC($challenge);
$this->info("Response: $response");
$this->info('Validating...');
$validation = $keyClaim->validate($challenge, $response);
if ($validation['valid']) {
$this->info('✓ Challenge validated successfully!');
} else {
$this->error('✗ Validation failed');
}
}
}Frontend Purpose Keys
php
// For frontend validation, use the frontend token endpoints
public function createFrontendChallenge($frontendToken, $ttl = 30)
{
$response = $this->client->post("$this->baseUrl/challenge/frontend-create", [
'json' => [
'token' => $frontendToken,
'ttl' => $ttl
]
]);
return json_decode($response->getBody(), true);
}
public function validateFrontendChallenge($frontendToken, $challenge)
{
$response = $this->client->post("$this->baseUrl/challenge/frontend-validate", [
'json' => [
'token' => $frontendToken,
'challenge' => $challenge
]
]);
return json_decode($response->getBody(), true);
}