Back to Documentation

Frontend Purpose API Keys

Secure client-side validation without exposing secrets

Client-Side API Key Usage Guide

Overview

All API keys in KeyClaim work the same way. There is no distinction between "frontend" and "backend" keys. This guide explains how to use API keys securely in client-side applications (web browsers, mobile apps) where you need to protect against MITM attacks and cannot securely store secrets.

Key Features

  • No Client-Side Secrets: The API key secret never leaves the server
  • Simple Token-Based Validation: Use a frontend token instead of generating HMAC responses
  • Server-Side Security: All cryptographic operations happen on the server
  • Same Security Level: Maintains the same security guarantees as backend keys

How It Works

  • Generate an API Key: Create an API key in your dashboard. For client-side usage, specify allowedOrigins for MITM protection.
  • Use Your API Key: Use your API key (starts with kc_) in client-side code - it's safe to expose as long as you configure allowedOrigins.
  • Create Challenge: Use your API key to create challenges. If you have a key pair assigned, challenges will be automatically encrypted.
  • Validate Challenge: Generate HMAC-SHA256 response on the client (using Web Crypto API) or decrypt if encrypted, then validate.
  • Origin Validation: If allowedOrigins is configured, the server validates that requests come from allowed domains only (MITM protection).

API Endpoints

All API keys use the same endpoints. See the main [API Documentation](/docs) for complete details.

Create Challenge

cURL Example:

bash
curl -X POST https://keyclaim.org/api/challenge/create \
  -H "Content-Type: application/json" \
  -H "Authorization: ApiKey kc_your_api_key_here" \
  -H "Origin: https://yourdomain.com" \
  -d '{
    "ttl": 30
  }'

Note: Include the Origin header if your API key has allowedOrigins configured. The API key should be in the Authorization header as ApiKey <your_key>.

Response:

json
{
  "challenge": "random_hex_string_or_encrypted",
  "expires_in": 30
}

Note: If your API key has a key pair assigned, the challenge will be automatically encrypted. The challenge value will be a base64-encoded encrypted string that you need to decrypt with your private key.

Validate Challenge

cURL Example:

bash
curl -X POST https://keyclaim.org/api/challenge/validate \
  -H "Content-Type: application/json" \
  -H "Authorization: ApiKey kc_your_api_key_here" \
  -H "Origin: https://yourdomain.com" \
  -d '{
    "challenge": "challenge_from_create",
    "response": "hmac_sha256_response_here"
  }'

Response:

json
{
  "valid": true,
  "signature": "signed_verdict_hex"
}

JavaScript Example

javascript
const API_KEY = 'kc_your_api_key_here';
const API_BASE_URL = 'https://keyclaim.org/api';

// Step 1: Create a challenge
async function createChallenge() {
  const response = await fetch(`${API_BASE_URL}/challenge/create`, {
    method: 'POST',
    headers: { 
      'Content-Type': 'application/json',
      'Authorization': `ApiKey ${API_KEY}`,
      'Origin': window.location.origin // Required if allowedOrigins is configured
    },
    body: JSON.stringify({
      ttl: 30
    })
  });
  
  const data = await response.json();
  return data.challenge;
}

// Step 2: Generate HMAC response
async function generateHMAC(challenge, secret) {
  const encoder = new TextEncoder();
  const data = encoder.encode(challenge);
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );
  const signature = await crypto.subtle.sign('HMAC', key, data);
  return Array.from(new Uint8Array(signature))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}

// Step 3: Validate the challenge
async function validateChallenge(challenge, secret) {
  const hmac = await generateHMAC(challenge, secret);
  
  const response = await fetch(`${API_BASE_URL}/challenge/validate`, {
    method: 'POST',
    headers: { 
      'Content-Type': 'application/json',
      'Authorization': `ApiKey ${API_KEY}`,
      'Origin': window.location.origin
    },
    body: JSON.stringify({
      challenge: challenge,
      response: hmac
    })
  });
  
  return await response.json();
}

// Complete flow
async function main() {
  try {
    // Create challenge
    const challenge = await createChallenge();
    console.log('Challenge:', challenge);
    
    // Generate HMAC (you need to store the secret securely)
    const secret = 'your_api_key_secret'; // Never expose this!
    const hmac = await generateHMAC(challenge, secret);
    
    // Validate
    const result = await validateChallenge(challenge, secret);
    console.log('Validation result:', result);
    
    if (result.valid) {
      console.log('✓ Challenge validated successfully!');
    }
  } catch (error) {
    console.error('Error:', error);
  }
}

React Example

jsx
import { useState } from 'react';

function ChallengeValidation() {
  const [challenge, setChallenge] = useState('');
  const [validating, setValidating] = useState(false);
  const [result, setResult] = useState(null);

  const API_KEY = 'kc_your_api_key_here';
  const API_BASE_URL = 'https://keyclaim.org/api';

  const createChallenge = async () => {
    const res = await fetch(`${API_BASE_URL}/challenge/create`, {
      method: 'POST',
      headers: { 
        'Content-Type': 'application/json',
        'Authorization': `ApiKey ${API_KEY}`,
        'Origin': window.location.origin
      },
      body: JSON.stringify({ ttl: 30 })
    });
    const data = await res.json();
    setChallenge(data.challenge);
  };

  const validateChallenge = async () => {
    if (!challenge) return;
    
    setValidating(true);
    try {
      // Generate HMAC
      const secret = 'your_api_key_secret'; // Store securely!
      const encoder = new TextEncoder();
      const data = encoder.encode(challenge);
      const key = await crypto.subtle.importKey(
        'raw',
        encoder.encode(secret),
        { name: 'HMAC', hash: 'SHA-256' },
        false,
        ['sign']
      );
      const signature = await crypto.subtle.sign('HMAC', key, data);
      const hmac = Array.from(new Uint8Array(signature))
        .map(b => b.toString(16).padStart(2, '0'))
        .join('');

      const res = await fetch(`${API_BASE_URL}/challenge/validate`, {
        method: 'POST',
        headers: { 
          'Content-Type': 'application/json',
          'Authorization': `ApiKey ${API_KEY}`,
          'Origin': window.location.origin
        },
        body: JSON.stringify({ challenge, response: hmac })
      });
      const data = await res.json();
      setResult(data);
    } finally {
      setValidating(false);
    }
  };

  return (
    <div className="p-6">
      <button 
        onClick={createChallenge}
        className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
      >
        Create Challenge
      </button>
      {challenge && (
        <>
          <p className="mt-4">Challenge: <code>{challenge}</code></p>
          <button 
            onClick={validateChallenge} 
            disabled={validating}
            className="mt-2 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50"
          >
            {validating ? 'Validating...' : 'Validate'}
          </button>
        </>
      )}
      {result && (
        <p className="mt-4">{result.valid ? '✓ Valid' : '✗ Invalid'}</p>
      )}
    </div>
  );
}

Security Considerations

  • API Key Exposure: While API keys can be exposed in client-side code, they are:
  • Rate-limited per IP
  • Monitored for suspicious activity
  • Can be revoked at any time
  • Protected by origin validation when allowedOrigins is configured
  • Challenge Replay: Challenges can only be used once and expire quickly (default 30 seconds)
  • IP Binding: For Business plans, you can optionally bind API keys to specific IP addresses
  • Rate Limiting: All requests are subject to rate limiting based on your plan

When to Use Client-Side API Keys

Use Client-Side API Keys When:

  • Building web applications (React, Vue, Angular, etc.)
  • Building mobile apps (React Native, Flutter, etc.)
  • You need secure validation without exposing secrets
  • You want MITM protection through origin validation

Use Backend API Keys When:

  • Building server-side applications
  • You can securely store API keys
  • You need full API access beyond challenge validation

MITM Protection

Client-side keys are protected from Man-in-the-Middle (MITM) attacks through Origin Validation:

json
   {
     "allowedOrigins": [
       "https://yourdomain.com",
       "https://app.yourdomain.com"
     ]
   }
  • Configure Allowed Origins: When generating an API key, specify allowedOrigins:
  • Automatic Validation: Server validates the Origin header on every request
  • Blocked Requests: Requests from unauthorized origins are rejected with origin_mismatch error

Important: API keys without allowedOrigins are vulnerable to MITM attacks. Always specify allowed origins for production keys.

See [Frontend MITM Protection Guide](./FRONTEND_MITM_PROTECTION.md) for complete details.

Error Codes

  • invalid_key: API key is invalid or inactive
  • expired_challenge: Challenge has expired
  • replayed_challenge: Challenge has already been used
  • rate_limited: Rate limit exceeded
  • security_check_failed: Security check failed (fraud detection, etc.)
  • origin_mismatch: Request origin doesn't match allowed origins (MITM protection)
  • missing_origin_header: Origin header is missing (required for keys with origin restrictions)