Back to Documentation
🔴

Laravel Integration

PHP Laravel framework integration guide

Laravel SDK Guide

Installation

bash
composer require guzzlehttp/guzzle

Service 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);
}