Sigma Auth
OAuth Features

The Auth Token API allows your backend services to generate short-lived, Bitcoin-signed authentication tokens without going through the full OAuth flow. This is ideal for server-to-server communication and API integrations.

Overview

Traditional OAuth is designed for user-facing applications, but backend services often need programmatic authentication. Sigma Auth's /api/auth/token-for-endpoint endpoint allows you to:

  • Generate tokens programmatically using Bitcoin signatures
  • Authenticate backend services without browser redirects
  • Integrate with existing API keys and service accounts
  • Maintain security through cryptographic signatures

Authentication Flow

1. Generate Bitcoin Signature

Your backend needs to sign a specific message format:

import { PrivateKey } from '@bsv/sdk';
import { SignedMessage } from 'bitcoin-auth';

async function generateAuthSignature(privateKey, endpoint) {
  // Create the message to sign
  const timestamp = Date.now();
  const message = `${endpoint}:${timestamp}`;
  
  // Sign with Bitcoin private key
  const signature = SignedMessage.sign(message, privateKey);
  
  return {
    message,
    signature,
    timestamp,
    pubkey: privateKey.toPublicKey().toString()
  };
}

// Example usage
const privateKey = PrivateKey.fromWif('your-service-account-wif');
const authData = await generateAuthSignature(privateKey, 'https://api.yourservice.com');

2. Request Authentication Token

Send the signature to Sigma Auth to get an access token:

async function getBackendToken(authData) {
  const response = await fetch('https://auth.sigmaidentity.com/api/auth/token-for-endpoint', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      endpoint: 'https://api.yourservice.com',
      signature: authData.signature,
      message: authData.message,
      pubkey: authData.pubkey,
      timestamp: authData.timestamp
    })
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Token generation failed: ${error.error_description}`);
  }

  const { access_token, expires_in } = await response.json();
  return { accessToken: access_token, expiresIn: expires_in };
}

3. Use Token for API Calls

Use the generated token to authenticate with your backend APIs:

async function makeAuthenticatedAPICall(accessToken, endpoint, data) {
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  });

  return response;
}

Complete Backend Service Example

Here's a full example of a backend service using auth tokens:

import { PrivateKey } from '@bsv/sdk';
import { SignedMessage } from 'bitcoin-auth';

class BackendAuthService {
  constructor(serviceWif, sigmaAuthUrl = 'https://auth.sigmaidentity.com') {
    this.privateKey = PrivateKey.fromWif(serviceWif);
    this.sigmaAuthUrl = sigmaAuthUrl;
    this.cachedToken = null;
    this.tokenExpiry = null;
  }

  async generateSignature(endpoint) {
    const timestamp = Date.now();
    const message = `${endpoint}:${timestamp}`;
    const signature = SignedMessage.sign(message, this.privateKey);
    
    return {
      message,
      signature,
      timestamp,
      pubkey: this.privateKey.toPublicKey().toString()
    };
  }

  async getToken(endpoint, forceRefresh = false) {
    // Return cached token if still valid
    if (!forceRefresh && this.cachedToken && this.tokenExpiry > Date.now() + 60000) {
      return this.cachedToken;
    }

    try {
      const authData = await this.generateSignature(endpoint);
      
      const response = await fetch(`${this.sigmaAuthUrl}/api/auth/token-for-endpoint`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          endpoint,
          signature: authData.signature,
          message: authData.message,
          pubkey: authData.pubkey,
          timestamp: authData.timestamp
        })
      });

      if (!response.ok) {
        const error = await response.json();
        throw new Error(`Auth failed: ${error.error_description}`);
      }

      const { access_token, expires_in } = await response.json();
      
      // Cache token with buffer time
      this.cachedToken = access_token;
      this.tokenExpiry = Date.now() + (expires_in * 1000) - 60000; // 1 minute buffer
      
      return access_token;
    } catch (error) {
      console.error('Token generation failed:', error);
      throw error;
    }
  }

  async authenticatedRequest(url, options = {}) {
    const token = await this.getToken(url);
    
    return fetch(url, {
      ...options,
      headers: {
        'Authorization': `Bearer ${token}`,
        ...options.headers
      }
    });
  }

  async authenticatedPost(url, data) {
    return this.authenticatedRequest(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
  }

  async authenticatedGet(url) {
    return this.authenticatedRequest(url, { method: 'GET' });
  }
}

// Usage
const authService = new BackendAuthService(process.env.SERVICE_ACCOUNT_WIF);

// Make authenticated requests
const response = await authService.authenticatedPost('https://api.yourservice.com/data', {
  message: 'Hello from backend service'
});

const data = await response.json();
console.log('API response:', data);

Express.js Middleware

Integrate backend auth tokens with Express.js:

import express from 'express';
import jwt from 'jsonwebtoken';

const app = express();

// Middleware to validate Sigma Auth tokens
function validateSigmaToken(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing or invalid Authorization header' });
  }

  const token = authHeader.split(' ')[1];

  try {
    // Validate the JWT token structure (Sigma Auth uses ES256)
    const decoded = jwt.decode(token, { complete: true });
    
    if (!decoded || decoded.header.alg !== 'ES256') {
      return res.status(401).json({ error: 'Invalid token format' });
    }

    // You can verify the token signature against Sigma Auth's public keys
    // For this example, we'll accept any valid JWT structure
    req.user = {
      pubkey: decoded.payload.sub, // Bitcoin public key
      bapId: decoded.payload.bap_id,
      endpoint: decoded.payload.aud // The endpoint this token was issued for
    };

    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

// Protected endpoint
app.get('/api/protected', validateSigmaToken, (req, res) => {
  res.json({
    message: 'Access granted',
    user: req.user,
    timestamp: Date.now()
  });
});

// Health check (unprotected)
app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

app.listen(3000, () => {
  console.log('Backend service running on port 3000');
});

Token Validation in Your API

Your backend API should validate tokens received from authenticated requests:

import jwt from 'jsonwebtoken';
import { PublicKey } from '@bsv/sdk';

class TokenValidator {
  constructor() {
    this.sigmaPublicKeys = null;
    this.keysFetchTime = 0;
  }

  async getSigmaPublicKeys() {
    // Cache keys for 1 hour
    if (this.sigmaPublicKeys && Date.now() - this.keysFetchTime < 3600000) {
      return this.sigmaPublicKeys;
    }

    try {
      const response = await fetch('https://auth.sigmaidentity.com/.well-known/jwks.json');
      const jwks = await response.json();
      
      this.sigmaPublicKeys = jwks.keys;
      this.keysFetchTime = Date.now();
      
      return this.sigmaPublicKeys;
    } catch (error) {
      throw new Error('Failed to fetch Sigma Auth public keys');
    }
  }

  async validateToken(token, expectedAudience) {
    try {
      const keys = await this.getSigmaPublicKeys();
      const decoded = jwt.decode(token, { complete: true });
      
      if (!decoded) {
        throw new Error('Invalid token format');
      }

      // Find the correct key
      const key = keys.find(k => k.kid === decoded.header.kid);
      if (!key) {
        throw new Error('Unknown token key');
      }

      // Verify token signature and claims
      const verified = jwt.verify(token, key.x5c[0], {
        algorithms: ['ES256'],
        audience: expectedAudience,
        issuer: 'https://auth.sigmaidentity.com'
      });

      return {
        valid: true,
        pubkey: verified.sub,
        bapId: verified.bap_id,
        endpoint: verified.aud,
        expiresAt: new Date(verified.exp * 1000)
      };
    } catch (error) {
      return {
        valid: false,
        error: error.message
      };
    }
  }
}

// Usage in API endpoint
const validator = new TokenValidator();

app.post('/api/secure-operation', async (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'Token required' });
  }

  const validation = await validator.validateToken(token, 'https://api.yourservice.com');
  
  if (!validation.valid) {
    return res.status(401).json({ error: validation.error });
  }

  // Token is valid, process the request
  res.json({
    success: true,
    user: validation.pubkey,
    data: 'Secure operation completed'
  });
});

Environment Variables

Set up your backend service with proper environment variables:

# .env file
SERVICE_ACCOUNT_WIF=L5oLkpV3aqBjhki6LmvChTCV6odsp4SXM6FfU2Gppt5kFLaHLuZ9
SIGMA_AUTH_URL=https://auth.sigmaidentity.com
API_BASE_URL=https://api.yourservice.com

# For development
# SERVICE_ACCOUNT_WIF=your-dev-wif
# SIGMA_AUTH_URL=http://localhost:1911

Security Considerations

Service Account Management

  • Rotate WIFs regularly: Generate new service account keys periodically
  • Secure storage: Use environment variables or secure key management systems
  • Separate keys per service: Don't reuse keys across different backend services
// Key rotation example
class RotatingAuthService extends BackendAuthService {
  constructor(primaryWif, secondaryWif = null) {
    super(primaryWif);
    this.secondaryKey = secondaryWif ? PrivateKey.fromWif(secondaryWif) : null;
    this.keyRotationDate = null;
  }

  async rotateToSecondaryKey() {
    if (!this.secondaryKey) {
      throw new Error('No secondary key configured');
    }
    
    // Switch to secondary key
    this.privateKey = this.secondaryKey;
    this.cachedToken = null; // Force token refresh
    this.keyRotationDate = new Date();
    
    console.log('Rotated to secondary key at', this.keyRotationDate);
  }
}

Token Security

  • Short expiration: Tokens expire in 30 minutes by default
  • Endpoint binding: Tokens are bound to specific endpoints
  • Signature verification: Always verify token signatures

Request Security

// Add request signing for extra security
function signRequest(method, url, body, privateKey) {
  const timestamp = Date.now();
  const bodyHash = crypto.createHash('sha256').update(body || '').digest('hex');
  const message = `${method}:${url}:${bodyHash}:${timestamp}`;
  
  return {
    signature: SignedMessage.sign(message, privateKey),
    timestamp,
    bodyHash
  };
}

// Use in requests
const requestSig = signRequest('POST', '/api/data', JSON.stringify(data), privateKey);

const response = await fetch('/api/data', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'X-Signature': requestSig.signature,
    'X-Timestamp': requestSig.timestamp,
    'X-Body-Hash': requestSig.bodyHash,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(data)
});

Error Handling

Handle common authentication token errors:

class AuthTokenError extends Error {
  constructor(message, code, statusCode = 500) {
    super(message);
    this.code = code;
    this.statusCode = statusCode;
  }
}

async function handleTokenErrors(operation) {
  try {
    return await operation();
  } catch (error) {
    if (error.message.includes('signature verification failed')) {
      throw new AuthTokenError('Invalid signature', 'INVALID_SIGNATURE', 401);
    }
    
    if (error.message.includes('timestamp too old')) {
      throw new AuthTokenError('Request expired', 'TIMESTAMP_EXPIRED', 401);
    }
    
    if (error.message.includes('rate limit')) {
      throw new AuthTokenError('Too many requests', 'RATE_LIMITED', 429);
    }
    
    if (error.message.includes('token expired')) {
      throw new AuthTokenError('Token expired', 'TOKEN_EXPIRED', 401);
    }
    
    throw new AuthTokenError('Authentication failed', 'AUTH_FAILED', 401);
  }
}

// Usage
try {
  const token = await handleTokenErrors(() => authService.getToken(endpoint));
  // Use token...
} catch (error) {
  if (error instanceof AuthTokenError) {
    console.error(`Auth error [${error.code}]:`, error.message);
    
    if (error.code === 'TOKEN_EXPIRED') {
      // Retry with force refresh
      const newToken = await authService.getToken(endpoint, true);
    }
  } else {
    console.error('Unexpected error:', error);
  }
}

Testing Backend Authentication

Test your backend authentication flow:

import { describe, it, expect, beforeAll } from '@jest/globals';

describe('Backend Authentication', () => {
  let authService;
  
  beforeAll(() => {
    authService = new BackendAuthService(process.env.TEST_SERVICE_WIF);
  });

  it('should generate valid signatures', async () => {
    const endpoint = 'https://test.example.com';
    const signature = await authService.generateSignature(endpoint);
    
    expect(signature.message).toContain(endpoint);
    expect(signature.signature).toBeDefined();
    expect(signature.pubkey).toBeDefined();
    expect(signature.timestamp).toBeInstanceOf(Number);
  });

  it('should obtain access tokens', async () => {
    const endpoint = 'https://test.example.com';
    const token = await authService.getToken(endpoint);
    
    expect(token).toMatch(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/); // JWT format
  });

  it('should cache tokens appropriately', async () => {
    const endpoint = 'https://test.example.com';
    
    const token1 = await authService.getToken(endpoint);
    const token2 = await authService.getToken(endpoint);
    
    expect(token1).toBe(token2); // Should return cached token
  });

  it('should make authenticated requests', async () => {
    const response = await authService.authenticatedGet('https://auth.sigmaidentity.com/userinfo');
    expect(response.ok).toBe(true);
    
    const data = await response.json();
    expect(data.pubkey).toBeDefined();
  });
});

Backend auth tokens provide a secure, programmatic way to authenticate your services with Sigma Auth without requiring user interaction. This pattern is perfect for server-to-server communication, automated processes, and API integrations while maintaining the security benefits of Bitcoin cryptographic signatures.